Preface

This developer guide comes directly from the docs directory of the Codename One Git repository. The documentation is written in AsciiDoc and published from the main branch to several targets.

Where else to look

This guide is the conceptual and tutorial reference. These resources cover the rest:

Authors

This document includes content from the following contributors and from earlier community wiki edits:

Rights & Licensing

You may copy, redistribute, or print this document without prior permission from Codename One. You may not charge for the document itself, but charging for printing costs remains permitted.

If you print or reproduce sections and make changes, state that the result is a modified copy. Also link to the original source at https://www.codenameone.com/developer-guide/.

Conventions

This guide uses several notations for tips and further guidance.

When additional information breaks from the current tutorial flow, use a sidebar like this:

Sidebar

Dig deeper into details that don’t quite fit the current flow. Sidebars cover important detours. You can skip them while reading and return to them later.

Here are common conventions for highlighting notes:

This is an important note
This is a helpful tip
This is a general informational note. It may be interesting, but it isn’t crucial.
This is a warning. Pay attention to it.

This convention is used when referring to a button or widget to press in the UI. For example, press the button labeled Press Here.

Quotes appear as follows:

This is a quote

— By Author

Bold provides light emphasis for a specific word or two within a sentence.

Getting started

1. Introduction

Codename One is a Write Once Run Anywhere mobile development platform for Java/Kotlin developers. It fits into modern Maven-capable IDEs such as IntelliJ IDEA, NetBeans, VS Code, and Eclipse, and you can also drive it entirely from the command line.

Codename One’s mission statement is:

Unify the complex and fragmented task of mobile device programming into a single set of tools, APIs and services. As a result create a more manageable approach to mobile application development without sacrificing the power/control given to developers.

This brings that old "Write Once Run Anywhere" (WORA) Java mantra to mobile devices without dumbing it down to the lowest common denominator.

The things that make Codename One stand out from other tools in this field are:

  • Write Once Run Anywhere support with no special hardware requirements and 100% code reuse

  • Compiles Java/Kotlin into native code for iOS, Android and even JavaScript/PWA

  • Open Source and Free with commercial backing/support

  • Easy to use with 100% portable Drag GUI builder

  • Full access to underlying native OS capabilities using the native OS programming language (for example: Objective-C) without compromising portability

  • Provides full control over every pixel on the screen

  • Lets you use native widgets (views) and mix them with Codename One components within the same hierarchy (heavyweight/lightweight mixing)

  • Supports seamless Continuous Integration out of the box

Codename One can trace its roots to the open source LWUIT project started at Sun Microsystem in 2007 by Chen Fishbein (co-founder of Codename One). It has been under constant development for over a decade.

1.1. Build cloud

The build cloud approach to mobile development is one of the things that makes Codename One stand out. iOS native development requires a Mac with Xcode. Windows native development requires a Windows machine. To make matters worse, Apple, Google, and Microsoft change their tools.

That makes it hard to keep up.

When you develop an app in Codename One, use the built-in simulator for running and debugging. When you want to build a native app, use the build cloud, where Macs build the native iOS apps and Windows machines build the native Windows apps. This works seamlessly and makes Codename One apps native because the native platform compiles them. For example, iOS builds use Macs running Xcode, the native Apple tool, to build the app.

Codename One doesn’t send source code to the build cloud, compiled bytecode!

1.1.1. Why build servers

The build servers can build native iOS apps without a Mac and native Windows apps without a Windows machine. They remove the need to install and update complex toolchains and simplify the process of building a native app to a right click.

Even though the build servers streamline delivery, Codename One also supports fully local builds. You can install the toolchain on your own hardware and follow the workflows in Chapter 2, Maven Project Workflow and Chapter 96, Working with Codename One sources to compile, package, and test apps without leaving your desktop environment.

For example, because native iOS applications require a Mac with a recent version of Xcode, Codename One maintains such machines in the cloud. When developers send an iOS build, that Mac generates C source code using ParparVM, then compiles the C source code using Xcode and signs the resulting binary using Xcode. You can install the binary on your device or build a distribution binary for the App Store. Since C code is generated, your app is also future-proof against changes from Apple. You can also inject Objective-C native code into the app while keeping it 100% portable thanks to Codename One’s native interfaces capability.

Subscribers can receive the C source code back using the include-sources feature of Codename One and use those sources for benchmarking and debugging on devices.

The same is true for most other platforms. On Android and Java SE, Codename One runs the standard Java code, unchanged.

Java Language Level Support

Historically, Java 8 syntax was supported through Retrolambda on the Codename One servers. It translated bytecode to Java 5 syntax using a server-based processor built on Retroweaver and custom code. This architecture was transparent to developers because the build servers abstract most of the painful differences between devices.

Today Codename One supports Java 25 level bytecode in ParparVM and Android supports Java 17 level bytecode natively. This lets Codename One support Java 17 seamlessly, and newer versions enable Java 25 level support through client side bytecode mutation.

1.1.2. How does Codename One work

Codename One uses a SaaS-based approach so the information in this appendix might (and probably will) change in the future to accommodate improved architectures. This appendix includes this information for reference, you don’t need to understand this to follow the content of the book…​

Since Android is already based on Java, Codename One is already native to Android and works with the Android VM (ART/Dalvik).

On iOS, Codename One built and open-sourced ParparVM, which is a conservative VM. ParparVM features a concurrent, non-blocking GC and is written entirely in Java/C. ParparVM is a transpiler that generates C source code matching the given Java bytecode. This means that an Xcode project is generated and compiled on the build servers. It’s as if you hand-coded a native app and is thus future-proof against changes that Apple introduces. For example, Apple migrated to 64-bit, then introduced bitcode, then withdrew bitcode again. ParparVM needed no modifications for any of those changes.

Codename One translates the bytecode to C, which is faster than Swift/Objective-C. The port code that invokes iOS APIs is hand coded in Objective-C

Codename One earlier offered a UWP (Universal Windows Platform) target based on iKVM. That target was discontinued in release 7.0.229 and is preserved as historical context in older documentation and blog posts.

JavaScript build targets translate the bytecode statically with ParparVM, the same translator that generates the C sources for iOS. Cloud builds keep the original TeaVM-based compiler as a compatibility fallback, which you select with the javascript.port build hint. To support the complex UI Codename One uses the HTML5 Canvas API which allows absolute flexibility for building applications.

Desktop builds are packaged on the same cloud machines, a Mac for the macOS installer and a Windows machine for the Windows one. The build originally used javapackager, which was abandoned because it couldn’t package a ZuluFX runtime reliably; installers are now produced directly with third-party packaging tools.

How one set of Java sources reaches each platform
Figure 1. What each target does with the same bytecode
Lightweight architecture

What makes Codename One stand out is the approach it takes to UI: “lightweight architecture.”

Lightweight architecture is the “not so secrete sauce” to Codename One’s portability. Essentially it means all the components/widgets in Codename One are written in Java. Thus their behavior is consistent across all platforms and they’re fully customizable from the developer code as they don’t rely on OS internal semantics. This allows developers to preview the application accurately in the simulators and GUI builders.

One of the big accomplishments in Codename One is its unique ability to embed “heavyweight” widgets into place among the “lightweights.” This is crucial for apps such as Uber where the cars and widgets on top are implemented as Codename One components yet below them, you have the native map component.

Codename One achieves fast performance by drawing using the native gaming APIs of most platforms for example: Metal on iOS. The core technologies behind Codename One are all open source including most of the stuff developed by Codename One itself, for example: ParparVM but also the full library, platform ports, designer tool, device skins etc.

Lightweight Architecture Origin

Lightweight components date back to Smalltalk frameworks, this notion was popularized in the Java world by Swing. Swing was the main source of inspiration to Codename One’s predecessor LWUIT. Many frameworks took this approach over the years including JavaFX and most recently Ionic in the JavaScript world.

Why ParparVM

On iOS, Codename One uses ParparVM which translates Java bytecode to C code and boasts a non-blocking GC. This VM is fully open source in the Codename One git repository. In the past Codename One used XMLVM to generate native code similarly, but the XMLVM solution was too generic for the needs of Codename One. ParparVM boasts a unique architecture of translating code to C (similarly to XMLVM), because of that Codename One is the only solution of its kind that can guarantee future iOS compatibility since the officially supported iOS toolchain is always used instead of undocumented behaviors.

XMLVM could guarantee that as well, but it’s no longer maintained and lacked the API layer support

The key advantages of ParparVM over other approaches are:

  • Truly Native: since code is translated to C rather than directly to ARM or LLVM code, the app is "more native." It uses the official tools and approaches from Apple and can gain from their advancements. For example: bitcode changes or profiling toolchain.

  • Smaller Class Library: ParparVM includes a small segment of the full JavaAPI’s resulting in final binaries that are smaller than the alternatives by orders of size. This maps directly to performance and memory overhead.

  • Simple and Extensible: to work with ParparVM you need a basic understanding of C. Unlike other tools that require deep understanding of ARM assembly and LLVM bitcode. This is crucial for the fast-moving world of mobile development, as Apple changes things left and right, you need a more agile VM.

Historical windows ports

Codename One experimented with many Windows ports over the years, including the legacy Windows Phone target and a later UWP (Universal Windows Platform) target based on iKVM.

The UWP target was discontinued in release 7.0.229 and is no longer part of the supported build targets. Older references to it in historical material are retained for archival context.
JavaScript port

The JavaScript port of Codename One uses ParparVM to translate Java bytecode into JavaScript. Cloud builds retain the original TeaVM-based builder as a compatibility fallback, selected with the public javascript.port=teavm build hint.

The JavaScript port allows unmodified Codename One applications to run within a desktop or mobile browser. The port itself is based on the HTML5 Canvas API, which provides a pixel-perfect implementation of the Codename One API.

The JavaScript port is available to all Codename One users.
Desktop and Android

The other ports of Codename One use the VMs available on the host machines and environments to execute the runtime. Historically, Retrolambda was used to provide Java 8 language features portably.

The Android port uses the native Android tools, including the Gradle build environment, in the latest versions.

The desktop port creates a standard Java SE application, which is packaged with the JRE and an installer.

The Desktop port is available to pro grade subscribers of Codename One

1.2. History

LWUIT App Screenshot
Figure 2. LWUIT App Screenshot circa 2007

Codename One was started by Chen Fishbein and Shai Almog who authored the Open Source LWUIT project at Sun Microsystems (circa 2007). The LWUIT project aimed to solve the fragmentation within J2ME/Blackberry devices by creating a higher standard of user interface than the common baseline at the time. LWUIT received critical acclaim and traction within many industries but was limited by the declining feature phone market. It was forked by many companies including Nokia. It was used as the base standard for DTV in Brazil. Another fork has brought a LWUIT into high-end cars from Toyota and other companies. This fork later adapted Codename One as well.

In 2012 Shai and Chen formed Codename One as they left Oracle. The project has taken many of the basic concepts developed within the LWUIT project and adapted them to the smartphone world which is still experiencing similar issues to the device fragmentation of the old J2ME phones.

1.3. Core concepts of mobile programming

Before you proceed, this section explains some universal core concepts of mobile programming that might not be intuitive. These are universal concepts that apply to mobile programming regardless of the tools you’re using.

You can skip this section if you feel you’re familiar enough with the core problems/issues in mobile app development.

1.3.1. Density

Density is also known as DPI (Dots Per Inch) or PPI (pixels or points per inch). Density is confusing, unintuitive and might collide with common sense. For example, an iPhone 7 plus has a resolution of 1080x1920 pixels and a PPI of 401 for a 5-inch screen. But, an iPad 4 has 1536x2048 pixels with a PPI of 264 on a 9.7 inch screen…​ Smaller devices can have higher resolutions!

As the following figure shows, if a Pixel 2 XL had pixels the size of an iPad, it would have been twice the size of that iPad. While in reality, it’s half the height of the iPad!

Device Density vs Resolution
Figure 3. Device Density vs. Resolution

Differences in density can be extreme. A second generation iPad has 132 PPI, where modern phones have PPI that’s over 600 dpi.

Low resolution images on high PPI devices will look either small or pixelated. High resolution images on low PPI devices will look huge, overscaled (artifacts) and will consume too much memory.

How the Same Image Looks in Different Devices
Figure 4. How the Same Image Looks in Different Devices

The exact same image will look different on each device, sometimes to a comical effect. One of the solutions for this problem is multi-images. All OS’s support the ability to define different images for various densities. Chapter 2 discusses multi-images later in detail.

This also highlights the need for working with measurements other than pixels. Codename One supports millimeters (or dips) as a unit of measurement. This is highly convenient and is a better representation of size when dealing with mobile devices.

However, there is a bigger conceptual issue involved. You need to build a UI that adapts to the wide differences in form factors. You might have fewer pixels on an iPad, but because of its physical size, you would expect the app to cram more information into that space so the app won’t feel like a blown-up phone application. Many strategies to address that, but one of the first steps is in the layout managers.

Chapter 2 discusses the layout managers in depth, but the core concept is that they decide where a UI element is placed based on generic logic. That way, the user interface can adapt automatically to the huge variance in display size and density.

1.3.2. Touch interface

The fact that mobile devices use a touch interface today isn’t news…​ However, the implications of that aren’t obvious to some developers.

UI elements need to be finger sized and spaced. Otherwise, you risk the “fat finger” effect. That means spacing should be in millimeters and not in pixels due to device density.

Scrolling poses another challenge in touch-based interfaces. In desktop applications, it’s common to nest scrollable items. For example, in touch interfaces, the scrolling gesture doesn’t allow such a nuance. Furthermore, scrolling on both the horizontal and vertical axis (side scrolling) can be inconvenient in touch-based interfaces.

1.3.3. Device fragmentation

Some developers single out this wide range of resolutions and densities as “device fragmentation.” While it does contribute to development complexity, it isn’t a challenging problem to overcome.

Densities aren’t the cause of device fragmentation. Device fragmentation is caused by many OS versions with different behaviors. This is clear on Android and relates to the slow rollout of Android vendor versions compared to Google rollout. A new release reaches only a small fraction of devices in its first year, and handsets several major versions behind stay in circulation long after their vendor stops shipping updates for them.

This makes QA difficult as the disparity between these versions is pretty big. It’s hard to get all device manufacturers aligned, so this problem will probably remain in the foreseeable future despite everything.

1.3.4. Performance

Besides the obvious need for performance and smooth animation within a mobile app, there are a couple of performance related issues that might not be intuitive to new developers: size and power.

App size

Apps are installed and managed through stores. This poses some restrictions about what an app can do. However, it also creates a huge opportunity. Stores manage automatic update and to some degree the marketing/monetization of the app.

A good mobile app is updated once a month and sometimes even once a week. Since the app downloads automatically from the store, this can be a huge benefit:

  • Existing users are reminded of the app and get new features instantly

  • New users notice the app featured on a “what’s new” list

If an app is big, it might not update over a cellular network connection. Google and Apple have restrictions on automatic updates over cellular networks to preserve battery life and data plans. A large app might negatively impact users perception of the app and trigger uninstalls for example, when a phone is low on available space.

Power drain

Desktop developers think about power usage within their apps. In mobile development, this is a crucial concept. Modern device OSes have tools that highlight misbehaving applications, and this can lead to bad reviews.

Code that loops forever while waiting for input will block the CPU from sleeping and drain the battery.

Worse. Mobile OSes kill applications that drain the battery. If the app is draining the battery and is minimized, (for example, during an incoming call) the app could be killed. This will impact app performance and usability.

1.3.5. Sandbox and permissions

Apps installed on the device are "sandboxed" to a specific area so they won’t harm the device or its functionality. The filesystem of mobile applications is restricted so one application can’t access the files of another application. Things that most developers take for granted on the desktop such as a "file picker" or accessing the image folder don’t work on devices!

This means that when your application works on a file, it belongs to your application. To share the file with a different application, you need to ask the operating system to do that for you.

Furthermore, some features require a "permission" prompt and sometimes require special flags in system files. Apps need to request permission to use sensitive capabilities, for example, Camera, Contacts etc.
Historically, Android developers declared required permissions for an app and the user was prompted with permissions during installation. Android 6 adopted the approach used by iOS of prompting the user for permission when accessing a feature.

This means that in runtime a user might revoke a permission. A good example for an Uber app is the location permission. If a user revokes that permission, the app might lose its location.

1.4. Installing Codename One

Codename One supports JDK 11 through 25 for running the simulator and the desktop ("Run as desktop app") target. Eclipse Temurin (https://adoptium.net) is the easiest way to install a supported JDK.

Codename One projects are built with Maven. Typical Maven targets such as package, clean and install work out of the box, but the Codename One integrations that ship with each IDE provide dedicated Run and Build actions for a smoother workflow.

To create a new Codename One project visit https://www.codenameone.com/initializr/ and generate a starter project, or run the Codename One Application Project Archetype (cn1app-archetype) directly on the command line:

mvn archetype:generate \
  -DarchetypeGroupId=com.codenameone \
  -DarchetypeArtifactId=cn1app-archetype \
  -DarchetypeVersion=LATEST \
  -DgroupId=YOUR_GROUP_ID \
  -DartifactId=YOUR_ARTIFACT_ID \
  -Dversion=1.0-SNAPSHOT \
  -DmainName=YOUR_MAIN_NAME \
  -DinteractiveMode=false

This command generates a project in the current directory. The folder name matches the artifactId value. For example, specifying -DartifactId=myapp produces a project inside a new myapp directory.

Import the generated Maven project into your preferred IDE and use the Codename One Run in Simulator task from the IDE toolbar or Run/Debug buttons:

  • IntelliJ IDEA: use File > Open on the project directory, then choose the Codename One Run in Simulator action from the toolbar or standard Run/Debug controls.

  • NetBeans: use File > Open Project, select the generated Maven project, and rely on the Codename One toolbar actions to run and debug the simulator.

  • VS Code: install the Java and Codename One extensions, open the folder, and trigger the Run in Simulator task from the command palette or the Run/Debug buttons.

  • Eclipse: use File > Import > Existing Maven Projects, then use the Codename One launch shortcuts provided by the plugin for simulator and build tasks.

  • Command line: invoke Maven goals directly whenever you need to integrate with CI/CD pipelines or scripting.

For deeper coverage of the Maven goals, project structure, and automation tasks, continue with Chapter 2, Maven Project Workflow.

Arbitrary Maven dependencies probably won’t work for Codename One. Many dependencies assume a full JDK which Codename One can’t provide and they often assume functionality that might not be available for example: reflection, Spring, etc.
Legacy onboarding resources

Legacy Ant-based project instructions remain available for teams maintaining older codebases. New projects should follow the Maven workflows described in this guide.

1.4.1. Important notes for new projects

Before you get to the code there are few important things you need to understand about Codename One applications.

  • App Name - This is the name of the app and the main class, it’s important to get this right as it’s hard to change this value later

  • Package Name - it’s crucial you get this value right. Besides the difficulty of changing this after the fact, once an app is submitted to iTunes/Google Play with a specific package name this can’t be changed! See the sidebar Picking a Package Name.

  • Theme - Codename One ships with various built-in themes; for simplicity recommend Native as it’s a minimal starting point

Picking a Package Name

Apple, Google and Microsoft identify applications based on their package names. If you use a domain that you don’t own it’s possible that someone else will use that domain and collide with you. In fact some developers left the default com.mycompany domain in place all the way into production sometimes.

This can cause difficulties when submitting to Apple, Google, or Microsoft. Submitting to one of them is no guarantee of success when submitting to another.

To come up with the right package name use a reverse domain notation. If your website is goodstuff.co.uk your package name should start with uk.co.goodstuff. Follow these guidelines for package names:

  • Lower Case - some OSes are case-sensitive and handling a mistake in case is painful. The Java convention is lower case; stick to that

  • Avoid Dash and Underscore - You can’t use a dash character (-) for a package name in Java. Underscore (_) doesn’t work for iOS. If you want more than one word just use a deeper package e.g.: com.mydomain.deeper.meaningful.name

  • Obey Java Rules - A package name can’t start with a number so you can’t use com.mydomain.1sler. You should avoid using Java keywords like this, if etc.

  • Avoid Top Level - instead of using uk.co.goodstuff use uk.co.goodstuff.myapp. That would allow you to have more than one app on a domain

1.4.2. Runtime

Once Maven is set up you can run the HelloWorld application by selecting the Codename One Run in Simulator task from the IDE run menu. The Codename One simulator launches and you can use its menus to control and inspect details related to the device. You can rotate it, determine its location in the world, monitor networking calls etc.

With the Skins menu you can download device skins to see how your app will look on different devices.

Some skins are bigger than the screen size, uncheck the Scrollable flag in the Simulator menu to handle them more effectively

Use your IDEs Debug button with the Run in Simulator task to launch the simulator under the debugger.

Simulator vs. Emulator

Codename One ships with a simulator similarly to the iOS toolchain which also has a simulator. Android ships with an emulator. Emulators go further. They create a virtual machine that’s compatible with the device CPU and then boot the full mobile OS within that environment. This provides a proper runtime environment but is slow.

Simulators rely on the fact that OSes are similar and so they leave the low-level details in place and just map the API behavior. Since Codename One relies on Java it can start simulating on top of the virtual machine on the desktop. That provides several advantages including fast development cycles and full support for all the development tools/debuggers you can use on the desktop.

Emulators make sense for developers who want to build OS level services for example, screensavers or low-level services. Standard applications are better served by simulators.

1.4.3. The source code of the hello world app

After clicking finish in the new project wizard you have a HelloWorld project with a few default settings. The following sections break the class down to small pieces and explain each piece starting with the enclosing class:

Listing 1. HelloWorld Class
public class HelloWorld extends Lifecycle {                             (1)

    @Override
    public void runApp() {                                              (2)
        Form hi = new Form("Hi World", BoxLayout.y());                  (3)
        Button helloButton = new Button("Hello World");
        hi.add(helloButton);
        helloButton.addActionListener(e -> hello());
        hi.getToolbar().addMaterialCommandToSideMenu("Hello Command",
                FontImage.MATERIAL_CHECK, 4, e -> hello());             (4)
        hi.show();
    }

    private void hello() {
        Dialog.show("Hello Codename One", "Welcome to Codename One", "OK", null);
    }
}
1This is the main class and the entry point to the app. It has no main method: the platform calls into Lifecycle, which calls you back.
2runApp() is where the app builds its first screen. Lifecycle calls it from start(), so this is the one method a new project overrides.
3Form is the “top level” UI element in Codename One. Only one is shown at a time and everything on the screen is a child of it. BoxLayout.y() stacks the children from the top down, and a theme decides how they look.
4The Toolbar carries the title bar and the side menu. The generated project puts the same action in both places so there is an example of each.

The four platform callbacks the Application Lifecycle Sidebar describes are already implemented for you by Lifecycle, which is why the generated class carries none of them:

  • init(Object) sets the network thread count, installs the theme, turns on the global Toolbar, binds crash protection through Log.bindCrashProtection and registers a network error listener, in that order.

  • start() calls runApp() on a cold start. On a warm start it re-shows the Form the user was looking at when the app was suspended, so returning to an app lands the user back where they were.

  • stop() remembers that Form, disposing a Dialog first so the app doesn’t come back to a modal.

  • destroy() does nothing by default.

Some complex ideas appear within this short snippet that this chapter addresses later when talking about layout. The gist of it’s that you create and show a Form, the top level UI element that takes over the whole screen. You add UI elements to it, here a Button that opens a Dialog when tapped, and the BoxLayout arranges them from top to bottom. The same action is attached to the side menu through the Toolbar, so the starter shows both ways of triggering one piece of code.

Application Lifecycle

A few years ago Romain Guy (a senior Google Android engineer) was on stage at the Google IO conference. He asked for a show of hands of people who understand the Activity lifecycle (Activity is like a Codename One main class). He then proceeded to jokingly call the audience members who lifted their hands “liars” claiming that after all his years in Google he still doesn’t understand it…​

Lifecycle seems simple on the surface but hides a lot of nuance. Android’s lifecycle is ridiculously complex. Codename One tries to simplify this and also make it portable. Sometimes complexity leaks out and the nuances can be difficult to deal with.

Explained an application has three states:

  • Foreground - it’s running and in the foreground which means the user can physically interact with the app

  • Suspended - the app isn’t in the foreground, it’s either paused or has a background process running

  • Not Running - the app was never launched, was killed or crashed

The lifecycle is the process of transitioning between these 3 states and the callbacks invoked when such a transition occurs. The first time you launch the app you start from a “Cold Start” (Not Running State) but on later launches the app is started from the "Warm Start" (Suspended State).

Codename One Application Lifecycle
Figure 5. Codename One Application Lifecycle

Codename One has four standard callback methods in the lifecycle API:

  • init(Object) - is invoked when the app is first launched from a Not Running state.

  • start() - is invoked for two separate cases. After start() is finished the app transitions to the Foreground state.

    • Following init(Object) in case of a cold start. Cold start refers to starting the app from a Not Running state.

    • When the app is restored from Suspended state. In this case init(Object) isn’t invoked

  • stop() - is invoked when the app is minimized for example, when switching to a different app. After stop() is finished the app transitions to the Suspended state.

  • destroy() - is invoked when the app is destroyed for example, killed by a user in the task manager. After destroy() is finished the app is no longer running hence it’s in the Not Running state.

destroy() is optional there is no guarantee that it will be invoked. It should be used only as a last resort

Override one when you need to run code at that point. What the super call protects differs by callback: in init it’s the whole list above, in start it’s the only thing that puts a Form on screen, and in stop it’s the record of which Form to come back to. destroy is empty, so super there is optional:

Listing 2. Overriding a lifecycle callback
public class HelloWorld extends Lifecycle {
    private TextArea editor;

    @Override
    public void init(Object context) {
        super.init(context);                                            (1)
        Log.p("app starting");
    }

    @Override
    public void stop() {
        saveDraft();                                                    (2)
        super.stop();
    }

    @Override
    public void runApp() {
        editor = new TextArea(Preferences.get("draft", ""));            (3)
        Form hi = new Form("Hi World", BoxLayout.y());
        hi.add(editor);
        hi.show();
    }

    private void saveDraft() {
        Preferences.set("draft", editor.getText());
    }
}
1Without this none of the five things init does happen, most visibly the theme, so the app comes up unstyled.
2Called as the app is suspended, which is the moment to persist anything the user would hate to lose.
3The other half of that: runApp puts the saved draft back, so the pair survives the app being killed while suspended.

That’s it. You should now have a general sense of the code. It’s time to run on the device.

1.4.4. Building and deploying on devices

Codename One Settings
Figure 6. Codename One Settings

Use the standalone Codename One Settings editor to configure the application title, version, package, icon, build hints, and extensions. Launch it from the Maven project root:

mvn cn1:settings

Use the standalone Certificate Wizard for signing assets. Monitor Codename One Cloud builds and subscription information on the Codename One website.

Signing/Certificates

All the modern mobile platforms require signed applications but they all take radically different approaches when implementing it.

Signing is a process that marks your final application for the device with a special value. This value (signature) is a value that you can generate based on the content of the application and your certificate. Effectively it guarantees the app came from you. This blocks a 3rd party from signing their apps and posing as you to the App Store or to the user. It’s a crucial security layer.

A certificate is the tool you use for signing. Think of it as a mathematical rubber stamp that generates a different value each time. Unlike a rubber stamp a signature can’t be forged!

Signing on Android
Backup your Android certificate and save its password!
If you lose your Android certificate you won’t be able to update your app

Android uses a self-signed certificate approach. You can generate a certificate by describing who you’re and picking a password!

Anyone can do that. For example, once a certificate is published it can’t be replaced…​

If this wasn’t the case someone else could push an “upgrade” to your app. Once an app is submitted with a certificate to Google Play this app can’t be updated with any other certificate.

With that in mind generating an Android certificate is trivial.

The following chart illustrates a process that’s identical on all IDEs
android certificate generator
Figure 7. Process of Certificate Generation for Android
Your certificate will generate into the file Keychain.ks in your home directory
Make sure to back that up and the password as losing these can have dire consequences
Should You Use a Different Certificate for Each App?

In theory yes. In practice it’s a pain…​ Keeping multiple certificates and managing them is a pain so you often just use one.

The drawback of this approach occurs when you’re building an app for someone else or want to sell the app. Giving away your certificate is akin to giving away your house keys. It makes sense to have separate certificates for each app.

Signing and provisioning iOS

Code signing for iOS relies on Apple as the certificate authority. This is something that doesn’t exist on Android. iOS also requires provisioning as part of the certificate process and separates the process for development/release.

First, the good news:

  • Losing an iOS certificate is no big deal - in fact you revoke them often with no impact on shipping apps

  • Codename One has a wizard that hides most of the pain related to iOS signing

In iOS Apple issues the certificates for your applications. That way the certificate is trusted by Apple and is assigned to your Apple iOS developer account. One important caveat applies: You need an iOS Developer Account and Apple charges a 99USD Annual fee for that.

The fee and the requirement have been part of the iOS developer program since it was introduced. Check Apple’s developer site for the current price.

Apple also requires a “provisioning profile” which is a special file bound to your certificate and app. This file describes some details about the app to the iOS installation process. One of the details it includes during development is the list of permitted devices.

The 4 files Required for iOS Signing and Provisioning
Figure 8. The Four Files Required for iOS Signing and Provisioning

You need 4 files for signing. Two certificates and two provisioning profiles:

  1. Production: The production certificate/provisioning pair is used for builds that are uploaded to iTunes

  2. Development: The development certificate/provisioning is used to install on your development devices

The standalone Certificate Wizard can automatically create these files and configure the current Maven project.

Run the wizard from the project directory:

mvn cn1:certificatewizard

After storing an App Store Connect API key, click Auto Setup in the toolbar. The wizard reads the project package and display name, creates or reuses the Bundle ID with push enabled, creates/reuses development and App Store signing assets, and writes the debug/release signing settings back into codenameone_settings.properties.

Certificate wizard overview with mocked signing assets
Figure 9. Certificate wizard overview with mocked signing assets
Certificate wizard profiles screen with mocked values
Figure 10. Certificate wizard profiles screen with mocked values

If you have more than one project you should use the same iOS P12 certificate files in all the projects and regenerate the provisioning. In this situation the Certificate Wizard asks you if you want to revoke the existing certificate which you shouldn’t revoke in such a case. You can update the provisioning profile in Apple’s iOS developer website.

One important aspect of provisioning on iOS is the device list in the provisioning step. Apple allows you to install the app on 100 devices during development. This blocks developers from skipping the App Store altogether. It’s important you list the correct UDID for the device in the list otherwise install will fail.

Many apps and tools offer the UDID of the device, but they aren’t necessarily reliable and might give a fake number!

The UDID section of the signing chapter covers the reliable ways to read that value.

Ad hoc provisioning allows 1000 beta testers for your application but it’s a more complex process that you won’t discuss here although it’s supported by Codename One
Build and install

Before you continue with the build you should sign up at https://www.codenameone.com/build-server.html where you can soon follow the progress of your builds. You need a Codename One account to build for the device.

Now that you have certificates, a device build is a single Maven goal. From the project’s root directory run mvn cn1:buildAndroid for Android, or mvn cn1:buildIos for an iOS debug build. Each one packages the app, sends it to the Codename One build servers and reports where the result lands.

Run these from the root, not from a module. The build goals skip any project that isn’t the execution root, so mvn -pl common cn1:buildAndroid prints “Skipping execution for non-root project” and then reports success without building anything.
The first time you send a build you will be prompted for the email and password you provided when signing up for Codename One

Once you send a build you should see the results in the build server page:

Build Results
Figure 11. Build Results
On iOS make sure you use Safari when installing, as 3rd party browsers might have issues

Once you go through those steps you should have the HelloWorld app running on your device. This process is non-trivial when starting so if you run into difficulties don’t despair and seek help at the discussion forum (https://www.codenameone.com/discussion-forum/) or stack overflow (https://stackoverflow.com/questions/tagged/codenameone). Once you go through signing and installation, it becomes easier.

You can also install the application either by emailing the installation link to your account (using the e-mail Link button)

You can also download the binaries to upload them to the appstores.

1.5. Kotlin

Codename One started before Kotlin became public. Kotlin has since shown itself as an interesting option for developers within the Android community. With that in mind, Codename One added support for Kotlin.

To use Kotlin with Codename One you can create a kotlin directory next to the java directory under the common/src/main directory. Kotlin code that resides there can work as usual and interact with the Java code.

Please notice the following:

  • Don’t use the project conversion tools or accept the warning that the project isn’t a Kotlin project. You do your own build process

  • Warnings and errors aren’t listed and builds that claim to have errors might pass

1.5.1. Hello Kotlin

Due to the way Kotlin works you can create a regular Java project and convert sources to Kotlin. You can mix Java and Kotlin code without a problem and Codename One would "work."

The Java source it converts from is the class shown in the walkthrough above: a Lifecycle subclass overriding runApp().

When you select that file and select the menu option Code → Convert Java file to Kotlin File you should get a familiar structure. The problem is that there are two bugs in the automatic conversion…​ That’s the code for Kotlin behaves differently from standard Java.

The first problem is that Kotlin classes are final unless declared otherwise so you need to add the open keyword before the class declaration.

This is essential as the build server will fail with weird errors related to instanceof.

This applies to the main class of the project, other classes in Codename One can remain final

The second problem only bites if you override init(Object), which the generated class doesn’t. Kotlin treats arguments as non-null by default, and the context passed to init can be null, so an override has to accept it as fun init(context: Any?) or it fails with an exception.

Once all that’s in place Kotlin should work. This should be possible for more JVM languages in the future.

2. Maven Project Workflow

Codename One uses Maven as the standard way to create, run, and maintain applications. This chapter consolidates the workflow guidance that used to live in the standalone Maven manual so the developer guide provides a single source of truth for building apps, updating projects, and managing add-ons.

2.1. Introduction

Codename One uses Maven as its primary build tool. This guide aims to be the definitive source of information for this project structure.

2.1.1. Conventions

The instructions throughout this chapter provide parallel guidance for the supported development environments. Each subsection heading identifies the target tooling so you can follow the directions that match your workflow:

  • Command Line (CLI): Focused on running Maven from a terminal. The commands use a Unix-style shell syntax; adapt the examples for Windows when necessary.

  • IntelliJ IDEA: Step-by-step directions for working inside IntelliJ.

  • NetBeans: Equivalent instructions tailored to NetBeans.

  • Eclipse: Maven-based guidance for Eclipse users.

When you reach environment-specific sections later in the guide, follow the subsection corresponding to your preferred tools.

2.2. Getting started

2.2.1. Prerequisites

Codename One supports JDK 11 through 25 for running the simulator and the "Run as desktop app" target. Eclipse Temurin is the easiest source of a supported JDK on macOS, Windows, and Linux.

After installing the JDK, point JAVA_HOME at it and confirm with:

java -version
mvn -v

Both should report Java 11 or newer. The Codename One Maven plugin checks the runtime JDK version when you invoke mvn cn1:run or mvn cn1:debug and fails fast with a friendly message if it’s older than 11. Build-only goals (such as -Pexecutable-jar) still work on older JDKs.

2.2.2. Creating a new project

Codename One initializr

The easiest way to create a new project is to use the Codename One initializr.

codenameone initializr screenshot

This tool will allow you to choose from a growing selection of project templates, and download a starter project that you can open in your preferred IDE (IntelliJ IDEA, NetBeans, etc.), or build directly on the command-line using Maven.

The initializr also offers Gradle as the build tool. A Gradle project is a single directory with a one-line settings file, and it gets native or backend directories only once it needs them. See Chapter 3, Gradle project workflow for when to choose it and how it maps to the Maven workflow in this chapter.

The following tutorials provide step-by-step instructions for getting started with bare-bones app templates. Those tutorials are a better starting place for Codename One development than this manual, as they’re written in tutorial form.

Java Getting Started Tutorial:

Kotlin Getting Started Tutorial:

Generating a new project from the command-line
Configuring the Codename One repository

Codename One releases are published to the Codename One Maven repository at https://repo.codenameone.com/maven2, not to Maven Central. A generated project declares that repository in its own pom.xml, so once you have a project nothing further is needed.

The commands in this section run before there is a project, so Maven has no pom.xml to read the repository from and searches only Maven Central. Central still serves every version published before the move, so these commands keep working, but they can’t see a release published after it. Add the repository to your Maven settings once, in ~/.m2/settings.xml (%USERPROFILE%\.m2\settings.xml on Windows), and they can:

<settings>
    <profiles>
        <profile>
            <id>codenameone</id>
            <repositories>
                <repository>
                    <id>codenameone</id>
                    <url>https://repo.codenameone.com/maven2</url>
                    <releases><enabled>true</enabled></releases>
                    <snapshots><enabled>false</enabled></snapshots>
                </repository>
            </repositories>
            <pluginRepositories>
                <pluginRepository>
                    <id>codenameone-plugins</id>
                    <url>https://repo.codenameone.com/maven2</url>
                    <releases><enabled>true</enabled></releases>
                    <snapshots><enabled>false</enabled></snapshots>
                </pluginRepository>
            </pluginRepositories>
        </profile>
    </profiles>
    <activeProfiles>
        <activeProfile>codenameone</activeProfile>
    </activeProfiles>
</settings>

Both lists are needed. Maven resolves ordinary dependencies through <repositories> and build plugins through <pluginRepositories>, and the commands below invoke the Codename One plugin directly, which is the second list.

Codename One Initializr needs none of this. It builds the project on the server and hands you one that already declares the repository.
Bare-bones Java project

If you prefer to generate your projects directly on the command-line, you can use the Section A.1, “Codename One application project archetype (cn1app-archetype)” to generate the project directly on the command-line:

mvn archetype:generate \
  -DarchetypeGroupId=com.codenameone \
  -DarchetypeArtifactId=cn1app-archetype \
  -DarchetypeVersion=LATEST \
  -DgroupId=YOUR_GROUP_ID \
  -DartifactId=YOUR_ARTIFACT_ID \
  -Dversion=1.0-SNAPSHOT \
  -DmainName=YOUR_MAIN_NAME \
  -DinteractiveMode=false

This will generate a project in the current directory. The project’s directory will have the same name as the artifact ID you specified here. For example, If your command had -DartifactId=myapp, then the project will be located in a newly created directory named "myapp."

If you haven’t used Maven archetypes before, this snippet may be confusing. See Introduction to Maven Archetypes to get up to speed.
-DarchetypeVersion=LATEST resolves against whichever repositories Maven knows about. Without the settings from Section 2.2.2.2.1, “Configuring the Codename One repository”, Maven Central is the only one, so the newest archetype it can offer is the last one published before the move. The generated project is still correct — it declares the Codename One repository, so mvn cn1:update moves it to a current release — but configuring the repository first gets you there directly.

This command uses the Section A.1, “Codename One application project archetype (cn1app-archetype)” which has the following Maven coordinates:

<dependency>
  <groupId>com.codenameone</groupId>
  <artifactId>cn1app-archetype</artifactId>
  <version>LATEST</version>
  <type>maven-archetype</type>
</dependency>

This archetype generates a bare-bones Java project (the same one described in Getting Started with the Bare-bones Java App Template).

View the source of the cn1app-archetype project here.

You can learn more about using the archetype in the appendix.

Project templates

The bare-bones Kotlin App project template is an alternative starter project that uses Kotlin as the primary language instead of Java. It’s built on the cn1app-archetype at its core, but it includes some more configuration settings and sources to change the template. You can use such templates as starter projects by using the generate-app-project goal of the Codename One Maven plugin.

Here is an example which generates a project based on the bare-bones Kotlin template:

mvn com.codenameone:codenameone-maven-plugin:{cn1-plugin-release-version}:generate-app-project \
  -DarchetypeGroupId=$archetypeGroupId \
  -DarchetypeArtifactId=$archetypeArtifactId \
  -DarchetypeVersion=$archetypeVersion \
  -DartifactId=$artifactId \
  -DgroupId=$groupId \
  -Dversion=$version \
  -DmainName=$mainName \
  -DinteractiveMode=false \
  -DsourceProject=/path/to/kotlin-example-app
This command is formatted for the bash prompt (for example, Linux or Mac). It will work on Windows also if you use bash. If you’re on Windows and are using PowerShell or the regular command prompt, then you’ll need to modify the command slightly. In particular, the entire command would need to be on a single line. (Remove the '\' at the end of each line, and merge lines together, with space between the command-line flags)

Like the archetype:generate goal, this will create the project in a directory named after your specified artifact ID. For example, If your command included -DartifactId=myapp, then the project would be in a newly created directory named "myapp."

Some notes here:

  1. The com.codenameone:codenameone-maven-plugin:7.0.210:generate-app-project argument is the fully qualified goal name for the generate-app-project. This is necessary since you aren’t running this goal in the context of any existing project. You should adjust the version number (7.0.210) to reflect the latest available Codename One version, listed as <release> in the repository metadata. Because there is no project here, Maven finds that version only if you have configured the Codename One repository in your Maven settings.

  2. The archetypeGroupId, archetypeArtifactId, and archetypeVersion parameters are the same as when using the archetype:generate goal, and they will (almost) always refer to the Section A.1, “Codename One application project archetype (cn1app-archetype)”.

  3. The groupId, artifactId, and version work the same as for the archetype:generate goal. That’s, that they specify the coordinates for your newly created project.

  4. The mainName specifies the Main class name for your app. This is the class name, and shouldn’t include the full package. For example, MyApp, not "com.example.MyApp"

  5. The sourceProject property is the path to the "template" project. In this case, you will assume that you’ve cloned the bare-bones Kotlin project template repository at /path/to/kotlin-example-app.

A project template isn’t much different than a regular project. The template can be either a legacy Ant project, or a new Maven project. In fact, this goal is the same one you would use to migrate a legacy Ant project to use the new Maven project structure.

See Section 2.4, “Creating project templates” for instructions on building your own project templates.

2.2.3. Migrating an existing project

If you have an existing Codename One application project that uses the old Ant project structure, you can use the generate-app-project goal to migrate the project over to maven. This goal doesn’t make any changes to the Ant project. It creates a new Maven project and copies over all the project sources and libraries, reorganized to fit the new project structure.

A minimal invocation of this goal would look like:

# Specify your the version of the codenameone-maven-plugin.
# Find the latest version at
# https://search.maven.org/search?q=a:codenameone-maven-plugin
CN1VERSION={cn1-plugin-release-version}
mvn com.codenameone:codenameone-maven-plugin:$CN1VERSION:generate-app-project \
  -DgroupId=YOUR_GROUP_ID \
  -DartifactId=YOUR_ARTIFACT_ID \
  -DsourceProject=/path/to/your/project \
  -Dcn1Version=$CN1VERSION
This command is formatted for the bash prompt (for example, Linux or Mac). It will work on Windows also if you use bash. If you’re on Windows and are using PowerShell or the regular command prompt, then you’ll need to modify the command slightly. In particular, the entire command would need to be on a single line. (Remove the '\' at the end of each line, and merge lines together, with space between the command-line flags)

This will generate the new project in the current directory inside a folder named after the artifactId parameter.

After building the project, try running it to make sure that the migration worked. For example, Assuming that your artifactId was myapp:

Command line
cd myapp
./run.sh
On Windows it would be run.bat instead of run.sh.

If All goes well, your app should open in the Codename One simulator.

IntelliJ IDEA

Open the myapp folder in IntelliJ. Then press the "Run" intellij run icon button in the upper right of the toolbar.

If All goes well, your app should open in the Codename One simulator.

NetBeans
Before opening the project in NetBeans, be sure to copy the files in the tools/netbeans directory into the root directory. These are necessary for NetBeans to run, build, and debug the project.

Open the myapp folder as a project in NetBeans. Then press the "Run" netbeans run icon button on the toolbar.

If all goes well it should open in the Codename One simulator.

Eclipse IDE

Open Eclipse, and select "File" > "Import…​"

eclipse file menu import

In the Import dialog, expand Maven, select Existing Maven Projects, and press Next.

eclipse import dialog

In the next panel, press the Browse button, and, in the file dialog, select the "myapp" directory, and press Next.

eclipse import browse dialog

The next panel should look like the one below. Make sure all the projects are "checked," and press Finish.

eclipse import list projects dialog

Almost there, but not quite…​

Next you need to import the Eclipse launch configurations located inside the tools/eclipse directory.

Select File > Import…​ again, but this time, in the Import dialog, select Run/Debug > Launch Configurations and click Next.

eclipse import launch configurations dialog

In the next panel, press Browse…​ then select the tools/eclipse directory.

eclipse launch configurations file dialog

Then check the eclipse option, and press Finish

eclipse import launch configurations finish

The "Run" button menu should now include options for all the major build targets. You can see them by pressing on the Run button in the toolbar:

eclipse run button dropdown

Select the MyApp - Run Simulator option from this menu.

If all goes well it should open in the Codename One simulator.

Example: Migrating kitchen sink app

Consider a concrete example, now. Download the KitchenSink Ant project from here and extract it.

The following is a bash script that uses curl to download this project as a zip file, and then converts it to a fully functional Maven project:

CN1_VERSION={cn1-plugin-release-version}
curl -L https://github.com/codenameone/KitchenSink/archive/v1.0-cn7.0.11.zip > master.zip
unzip master.zip
rm master.zip
mvn com.codenameone:codenameone-maven-plugin:${CN1_VERSION}:generate-app-project \
  -DarchetypeGroupId=com.codename1 \
  -DarchetypeArtifactId=cn1app-archetype \
  -DarchetypeVersion=${CN1_VERSION} \
  -DartifactId=kitchensink \
  -DgroupId=com.example \
  -Dversion=1.0-SNAPSHOT \
  -DinteractiveMode=false \
  -DsourceProject=KitchenSink-1.0-cn7.0.11
This command is formatted for the bash prompt (for example, Linux or Mac). It will work on Windows also if you use bash. If you’re on Windows and are using PowerShell or the regular command prompt, then you’ll need to modify the command slightly. In particular, the entire command would need to be on a single line. (Remove the '\' at the end of each line, and merge lines together, with space between the command-line flags)

This will generate the maven project in a directory named "kitchensink" in the current working directory because of the -DartifactId=kitchensink directory.

2.2.4. Adding project dependencies

For the easiest and recommended approach to adding dependencies to your project, skip to Section 2.2.4.5, “Managing add-ons in Codename One Settings”.

One of the reasons to use Maven as the build tool is because it makes the management of project dependencies almost trivial. If the library you want to add is on Maven central, then you can copy and paste its <dependency> snippet into your pom.xml file and you’re good to go. Maven does the rest.

See Introduction to the Dependency Mechanism on the Maven website for a gentle, but comprehensive introduction to Maven dependencies.

With Codename One projects, there are a few caveats (see Section 2.2.4.4, “The compliance check”), and a few added niceties that make it easier to find and install add-on libraries in your project (see Section 2.2.4.5, “Managing add-ons in Codename One Settings”).

Which pom.xml to add the <dependency> snippet to

Suppose you have a Maven <dependency> snippet that you’ve copied from Maven central, and it’s burning a hole in your clipboard while you’re trying to figure out where to paste it into your project. Codename One application projects are multi-module projects, so they have more than one pom.xml file: the root one, the common module’s, and one per platform module the project has. A project generated with the defaults has no platform modules (see Section A.1.1, “Projects without platform modules”).

Question: Which pom.xml file should you paste the snippet into?

Answer: common/pom.xml (almost always).

The "common" module is where all your Codename One application resides. It houses your Java and Kotlin files, your CSS files, your GUI builder files, your Codename One configuration files (that is, codenameone_settings.properties). Pretty much everything. The things you’d place in the other modules (for example, Java SE, ios, etc.) are your platform-specific native interface implementations; And in many applications you won’t need any of that.

When adding dependencies into your app, you’ll therefore almost always place them inside the pom.xml file for the "common" module.

You can add dependencies without editing XML by launching Codename One Settings with mvn cn1:settings. See Section 2.2.4.5, “Managing add-ons in Codename One Settings”.
When to use the "other" pom.xml Files

The instructions say that you almost always add dependencies in the common/pom.xml file. What are the other modules' pom.xml files for, and when do you need to change them, or add dependencies to them?

Here’s an overview:

%PROJECT_ROOT%/pom.xml

The root pom.xml file is the parent module of all other modules. Anything you add here will be inherited by all the modules. It can be helpful to use <dependencyManagement> and <pluginManagement> sections in this file to merge versions for dependencies and plugins project-wide. This is also a good place to add project meta-data like <developers>, <scm>.

javase/pom.xml (when the project has a javase module)

Any dependencies that are only required for native implementations on the Java SE platform can be added here. Dependencies added to this project aren’t subject to the compliance check.

Additionally, this module handles the build toolchain for the Java SE platform. This includes Mac and Windows Desktop builds, as well as Java SE desktop builds. If you want to customize the build workflow for any of these targets, you would do so by adding plugin executions in this pom.xml file.

android, ios, win, linux, and javascript (when the project has them)

These modules don’t use Maven for their dependencies (Android may deserve a small asterisk here, but that’s complicated), so the primary thing you’d want to change in these pom.xml files are the build toolchain for those targets. For example, you might add plugin executions for your CI workflow on builds targeting these particular platforms.

A project without these modules builds every platform from the common module. To customize one platform’s build, add its module: a <platform>/pom.xml copied from a project generated with -DplatformModules=all. The common module stops building that platform as soon as the module exists.

Example: Adding Google Maps dependency via Maven central

Add the GoogleMaps library to your app as a maven dependency.

As described in the GoogleMaps cn1lib README, the dependency snippet is:

<dependency>
  <groupId>com.codenameone</groupId>
  <artifactId>googlemaps-lib</artifactId>
  <version>1.0.1</version>
  <type>pom</type>
</dependency>

You should, but, look on Maven central to see what the latest version number is, and substitute that version into the <version> tag of the snippet.

Copy and paste this snippet into the <dependencies> section of your common/pom.xml file. And save it.

The common/pom.xml file has a lot of existing configuration in it, and it may not be clear, on first glance, where the <dependencies> tag is located. A simple "find" for <dependencies> may also lead you astray, since there are a few <profile> tags which also include <dependencies> sections.

The correct <dependencies> section, is located near the top of the file. You can identify it because it will include the following comment:

<!-- INJECT DEPENDENCIES -->

This is a special marker that’s used by some Codename One tooling to help it locate the optimal place to inject dependencies.

Don’t REMOVE THIS COMMENT. Just add your dependency snippet somewhere before or after it.

Compatibility with Codename One

You can paste any Maven dependency snippet you like into your project, but libraries that haven’t been specifically developed for Codename One might not be compatible. See Appendix C, API. If you’re unsure whether a library is compatible, you could add the dependency and try to use it in your app. If it isn’t compatible, it will fail when you try to build the app, during the compliance check.

The easiest way to find Codename One libraries is to use the Extensions view in Codename One Settings. These libraries were built specifically for Codename One. Settings warns before installing catalog entries known to target older Codename One releases.

The compliance check

All application code in the common module of your Codename One project must be compatible with Codename One. This includes all dependencies. When you build your project, it will perform a compliance check to ensure that no code uses unsupported APIs. (See Appendix C, API).

If the compliance check fails (that is, the app uses unsupported APIs), the build will fail. The error log should provide some clues about where the offending code resides.

Managing add-ons in Codename One Settings

Launch Codename One Settings from the project root, then select Extensions in the left navigation rail:

mvn cn1:settings
Codename One Settings Extensions view

As an example, install the "Google Maps" library.

Type Maps in the search field and locate Codename One Google Native, the Google Maps library.

Click Install or Download, depending on how the catalog entry is distributed.

Settings shows progress while it installs the extension. For an installed extension, click the same action area and confirm Uninstall to remove it.

How Codename One Settings handles Maven dependencies

Many extensions listed in Settings are deployed as cn1lib bundles. Others are deployed on Maven Central and could be installed by adding a snippet to pom.xml (as described in Section 2.2.4.2, “Example: Adding Google Maps dependency via Maven central”).

Settings shields you from the details of how it installs extensions. For extensions deployed on Maven Central, it adds the dependency directly to your project’s common/pom.xml. For extensions distributed as cn1lib bundles, it uses the install-cn1lib Maven goal.

You shouldn’t need to worry about this, as it happens seamlessly. If you’re curious, you can look at the <dependencies> section of your common/pom.xml file to see the added <dependency> tag after you install an extension.

If the Settings window opens blank

Settings is a Codename One app hosted in a native window, so a window that opens but shows nothing can come from the window layer, the display scale, the theme, or font loading. Capture the render state and attach it to a bug report rather than guessing:

mvn cn1:settings -Dsettings.diagnostics=cn1-settings-diagnostics.txt

The file records the OS and JDK, the HiDPI scale the JVM reports, the window and canvas geometry, the size Codename One laid the form out at, and the colours and font metrics resolved for the theme. Add -Dsettings.screenshot=shot.png to also write the window contents; a second shot.onscreen.png captures the pixels actually on screen, which can differ from the offscreen paint. The launch log is at ~/.codenameoneSettings/settings.log.

Installing legacy cn1libs

The recommended approach for installing add-ons is to use Codename One Settings or add the Maven dependency to common/pom.xml. In some situations neither method is available. For example, you might have a legacy cn1lib file that isn’t listed in the extension catalog or deployed to Maven Central.

In cases like this you can use the install-cn1lib Maven goal to install it as follows:

mvn cn1:install-cn1lib -Dfile=/path/to/yourlibrary.cn1lib

2.3. Updating Codename One

Codename One releases new versions weekly to the Codename One Maven repository. It’s recommended that you stay up to date with the latest version as much as possible to ensure compatibility with the Codename One build server, which is always running the latest version.

You can use the update goal to update both the Codename One libraries, and the Codename One dependencies in your project.

For example:

mvn cn:update

CLI

Or you can use the run.sh/run.bat script to run this goal as follows:

./run.sh update
Use run.bat update instead on Windows

IntelliJ

Or you can click on the "Configuration" menu, and select "Tools" > "Update Codename One" as shown here:

intellij update codenameone

Then press the Run button.

NetBeans

Or you can right-click on the project in the project inspector, and select "Run Maven" > "Update Codename One" as shown here:

netbeans update codenameone

Then press the Run button.

Manually updating the pom.xml file

You can also update your Codename One dependencies manually by modifying the cn1.version and cn1.plugin.version properties defined in your project’s pom.xml file.

For example, Open the pom.xml file, and look for the following:

<cn1.plugin.version>7.0.210</cn1.plugin.version>
<cn1.version>7.0.210</cn1.version>

Change these values to reflect the latest version of the codenameone-maven-plugin, listed as <release> in the repository metadata. The update goal reads the same metadata and edits both properties for you.

Updating the cn1.plugin.version and cn1.version properties manually will update the Maven dependencies for your project but it won’t update the other Codename One tools such as the GUI builder, and the Build Server Client, which are managed outside of Maven. You should use the Section B.23, “Update Codename One (update)” as described at the beginning of this section to perform a "full" update.

2.4. Creating project templates

A project template is a Codename One application project that can be used as a starting point for building a Codename One application. Codename One initializr uses project templates to generate starter projects for Codename One applications. You can also use the Section B.5, “Generate app project (generate-app-project)” goal to generate starter projects from templates directly in Maven.

Any Codename One project can be converted into a project template.

2.4.1. Converting a Codename One application project into a project template

If you have an existing maven Codename One application project, you can convert it into a project template by adding a file named generate-app-project.rpf in the root directory of the project.

The contents of this file should look like:

template.mainName=$YOUR_PROJECT_MAIN_NAME
template.packageName=$YOUR_PROJECT_PACKAGE_NAME

[dependencies]
====
... YOUR PROJECT MAVEN DEPENDENCIES ...
====

[parentDependencies]
====
... YOUR PARENT PROJECT MAVEN DEPENDENCIES ...
====

Where you make the following substitutions:

$YOUR_PROJECT_MAIN_NAME

This should be the value of the codename1.mainName property in the project’s codenameone_settings.properties file.

$YOUR_PROJECT_PACKAGE_NAME

This should be the value of the codename1.packageName property in the project’s codenameone_settings.properties file.

…​ YOUR PROJECT MAVEN DEPENDENCIES…​

Paste any maven dependencies that the project requires into this section. These will be injected into the <dependencies> section of the common/pom.xml file.

…​ YOUR PARENT PROJECT MAVEN DEPENDENCIES…​

Paste any maven dependencies that the parent project requires into this section. These will be injected into the <dependencies> section of the pom.xml file.

See Section B.5.4.2, “Sample generate-app-project.rpf file” for a more concrete example of the generate-app-project.rpf.

2.4.2. Test your project template

You can test your project template by using it as the sourceProject parameter for the generate-app-project goal. See Section B.5, “Generate app project (generate-app-project)”.

2.5. Codename One libraries

A Codename One Library (cn1lib) is a module that can be distributed and added to Codename One applications to add functionality. It can be distributed as a self-contained bundle (a file with the.cn1lib extension), or deployed on Maven central to be included in application projects as a pom dependency.

A cn1lib may contain any of the following:

  1. Cross-platform Java classes.

  2. Native code that targets specific platforms.

  3. Build hints, which will affect how projects will be built that include this library. These can contain things like Gradle dependencies on Android, Cocoapods dependencies on iOS, and other hints to affect the build-server process.

  4. CSS files.

.cn1lib vs .jar

You may be wondering why the .cn1lib format is even necessary. Why not just distribute libraries as .jar files? The .cn1lib format offers several advantages over the plain .jar format:

  1. cn1libs can contain platform-specific native sources that make use of native APIs on the various platforms. For example, they can contain Objective-C code which will be compiled on the build server when deploying on iOS.

  2. Codename One library projects perform a compliance check at the time that the library is compiled to ensure it only uses supported Codename One APIs. This provides a sort of "certification" that the library will be compatible with Codename One application projects.

  3. Codename One libraries can include CSS files and build hints which will be appended to the build hints of application projects when they’re built.

All that said, you can still distribute libraries as plain old jars and include them in your Maven Codename One projects as jar dependencies. Codename One application projects will perform an additional compliance check to ensure that the jar is compatible, and the build will fail if it uses APIs that aren’t available in Codename One.

Codename One supports a subset of JavaSE8, as well as addition APIs for accessing device functionality and building beautiful user interfaces. See the Codename One javadocs for a definitive list of supported APIs.

2.5.1. Creating a library project

Use the cn1lib-archetype for generating a new Codename One library project as follows:

Command line
mvn archetype:generate \
  -DarchetypeArtifactId=cn1lib-archetype \
  -DarchetypeGroupId=com.codenameone \
  -DarchetypeVersion=LATEST \
  -DgroupId=com.example.mylib \
  -DartifactId=mylib \
  -Dversion=1.0-SNAPSHOT \
  -DinteractiveMode=false
This command is formatted for the bash prompt (for example, Linux or Mac). It will work on Windows also if you use bash. If you’re on Windows and are using PowerShell or the regular command prompt, then you’ll need to modify the command slightly. In particular, the entire command would need to be on a single line. (Remove the '\' at the end of each line, and merge lines together, with space between the command-line flags)

In the above snippet you would change the groupId, artifactId, and version properties to reflect your project settings.

You can run the archetype:generate goal with as many or few properties as you like, and it will prompt you to enter any properties that are required. For example, You could enter:

mvn archetype:generate

And then follow the prompts. Or you could enter:

mvn archetype:generate -DarchetypeGroupId=com.codenameone \
  -DarchetypeArtifactId=cn1lib-archetype
This command is formatted for the bash prompt (for example, Linux or Mac). It will work on Windows also if you use bash. If you’re on Windows and are using PowerShell or the regular command prompt, then you’ll need to modify the command slightly. In particular, the entire command would need to be on a single line. (Remove the '\' at the end of each line, and merge lines together, with space between the command-line flags)

And follow the prompts. This will, result in fewer prompts because you’ve already specified the archetype to use.

This will create a new project for you in the current directory, in a newly created directory named after the artifactId that you entered.

IntelliJ IDEA
  1. Select "File" > "New Project"…​

    intellij new project menu
  2. Select "Maven" in the left menu.

    intellij new project dialog maven
  3. Check the "Create from Archetype" checkbox intellij create from archetype checkbox. This should allow you to choose from the archetypes that are already known to IntelliJ.

  4. If you don’t see an option for com.codenameone:cn1lib-archetype, then IntelliJ doesn’t know about it yet. If, but you do see this option, you can skip to the next step. Press the "Add Archetype…​" button. This will display a dialog for you to enter the details of the archetype.

    intellij add archetype

    Fill in this dialog as shown in the above image. Specifically groupId=com.codenameone, artifactId="cn1lib-archetype," and version="LATEST"

    Then press OK.

  5. Select the option that says "com.codenameone:cn1lib-archetype"

    intellij select cn1lib archetype

    Then press "Next"

  6. This will display a form where you can enter the details of your project such as its location (where you want to create the project folder), the name, the artifact ID, and the groupID. Fill in this form as you see fit.

    intellij new cn1lib project details form

    Then click "Next"

  7. The final form in this wizard summarizes the project details and gives you an opportunity to add more properties to pass to the archetype:generate goal. In your case you don’t need to add any more properties. If the information looks correct, you can press Next.

    intellij new cn1lib project details summary

At this point you will be prompted to open the project.

NetBeans
  1. Select "File" > "New Project…​"

    netbeans new project menu
  2. In the "New Project" dialog, select "Java with Maven" in the left panel, and "Project from Archetype" in the right panel, as shown below.

    netbeans new project maven dialog

    Then press "Next"

  3. This will bring you to the "Maven Archetype" dialog as shown below:

    netbeans maven archetype cn1lib dialog

    Enter "com.codenameone" or "cn1lib-archetype" into the search field. Then select "cn1lib-archetype" in the "Known archetypes:" panel. This will prefill the Group ID, Artifact ID and Version fields for you. You may want to change Version to LATEST to ensure that it tries to use the latest available version of the archetype.

    Then click "Next"

  4. This will bring you to the "Name and Location" panel of the wizard.

    netbeans new project name and location

    Enter in the project name (which you’ll be forced to use as the artifact ID also), project location, groupId, version, and package. The "Package" is unimportant here as it isn’t used anywhere in the project.

    Once you’ve entered the information to your liking press the "Finish" button.

This will create a new library project for you at the location you specified.

Eclipse IDE
  1. Select "File" > "New Project…​"

  2. In the New Project dialog, expand the Maven item, and select Maven Project

    eclipse new project wizard

    Then press "Next"

  3. The next panel will look like the below image. The default settings on this panel should be fine. Press Next

    eclipse new project wizard new maven project
  4. In the next panel, enter "cn1lib" in the Filter field. After a moment the cn1lib-archetype should appear in the area below as shown here:

    eclipse new maven project cn1lib

    Select that option, and press Next

  5. The next panel, allows you to enter your project details, such as group ID, and artifact ID. Your project information here and then press Finish.

    eclipse new maven project details

This will create a new library project for you at the location you specified.

Project structure

Take a look at the project that was created. It’s a multi-module Maven project with the following modules:

common

The module where you’ll add all your cross-platform code and CSS, and build hint configuration. This module is in the "common" directory of the main project.

Java SE

The module where you can implement native interfaces for the Java SE platform. This module is in the "Java SE" directory of the main project.

ios

The module where you can implement native interfaces for the iOS platform. This module is in the "ios" directory of the main project.

android

The module where you can implement native interfaces for the Android platform. This module is in the "android" directory of the main project.

JavaScript

The module where you can implement native interfaces for the JavaScript platform. This module is in the "JavaScript" directory of the main project.

lib

The library module which includes all the other modules as dependencies, and can be used as a pom dependency in Codename One application projects that wish to use this library. This module is in the "lib" directory of the main project.

tests

An application project for writing unit tests against your library. This module is in the "tests" directory of the main project.

IntelliJ IDEA

The project inspector will look like:

intellij myfirstlib project inspector

This top-level view of the module structure may seem daunting. Most of your development will occur inside the "common" module. If you expand that module it will look more familiar to developers who have used the old Ant project structure:

intellij myfirstlibrary common project files

Your cross-platform Java source would go in the common/src/main/java directory. Your CSS files go in the common/src/main/css directory.

NetBeans

The project inspector will look like:

netbeans myfirstlibrary project inspector

This top-level view of the modules doesn’t provide a clear view of the project landscape, but, since 99% of your development will occur inside the common submodule. Open that "common" sub-module project as well and take a peek.

Right-click on the "Common" sub-module, and select "Open Project" as shown below:

netbeans myfirstlibrary open common submodule

With the common subproject open, the project inspector will look like:

netbeans myfirstlibrary project inspector with common

In this screenshot, "Source Packages" and "Other Sources/css" are expanded to highlight where your Java source files and CSS source files will be located.

The project inspector hides a few important files, but, so here is a screenshot of the File inspector for the common project:

netbeans my first library file inspector common
Eclipse IDE

The package explorer will look like:

eclipse cn1lib package explorer

In this screenshot, the common/src/main/css and common/src/main/java directories are expanded since this is where most of your module source will go.

Command line

If you do a file listing on the project directory, it shows the following:

Steves-Mac-Pro:MyFirstLibrary shannah$ find .
.
./tests
./tests/pom.xml
./tests/javase
./tests/javase/pom.xml
./tests/common
./tests/common/codenameone_settings.properties
./tests/common/pom.xml
./tests/common/nbactions.xml
./tests/common/src
./tests/common/src/test
./tests/common/src/test/java
./tests/common/src/test/java/com
./tests/common/src/test/java/com/example
./tests/common/src/test/java/com/example/myfirstlib
./tests/common/src/test/java/com/example/myfirstlib/MyFirstTest.java
./tests/common/src/main
./tests/common/src/main/css
./tests/common/src/main/css/theme.css
./tests/common/src/main/java
./tests/common/src/main/java/com
./tests/common/src/main/java/com/example
./tests/common/src/main/java/com/example/myfirstlib
./tests/common/src/main/java/com/example/myfirstlib/LibraryTests.java
./tests/cn1libs
./tests/.mvn
./tests/.mvn/jvm.config
./pom.xml
./javase
./javase/pom.xml
./javase/src
./javase/src/main
./javase/src/main/java
./javase/src/main/java/com
./javase/src/main/java/com/example
./javase/src/main/java/com/example/myfirstlib
./ios
./ios/pom.xml
./ios/src
./ios/src/main
./ios/src/main/objectivec
./common
./common/codenameone_library_required.properties
./common/pom.xml
./common/codenameone_library_appended.properties
./common/src
./common/src/test
./common/src/test/java
./common/src/test/java/com
./common/src/test/java/com/example
./common/src/test/java/com/example/myfirstlib
./common/src/test/java/com/example/myfirstlib/MyLibraryTest.java
./common/src/main
./common/src/main/css
./common/src/main/css/theme.css
./common/src/main/java
./common/src/main/java/com
./common/src/main/java/com/example
./common/src/main/java/com/example/myfirstlib
./common/src/main/java/com/example/myfirstlib/MyLibrary.java
./android
./android/pom.xml
./android/src
./android/src/main
./android/src/main/java
./android/src/main/java/com
./android/src/main/java/com/example
./android/src/main/java/com/example/myfirstlib
./lib
./lib/pom.xml
./MyFirstLibrary.iml
./javascript
./javascript/pom.xml
./javascript/src
./javascript/src/main
./javascript/src/main/javascript
./.idea
./.idea/encodings.xml
./.idea/jarRepositories.xml
./.idea/.gitignore
./.idea/workspace.xml
./.idea/misc.xml
./.idea/compiler.xml

This may seem daunting at first, but it’s important to realize that 99% of the time, you’ll be working in the "common" module - most of the other stuff is boilerplate.

Important files

A few key files in this project that you’ll be using more than the others.

pom.xml

The maven configuration file of the root module is where you will set project-wide properties such as the cn1.version property, which specifies the version of the Codename One libraries that the module should be compiled against. Periodically, you’ll want to update the cn1.version property to point to the latest version.

When/if you decide to deploy your module to Maven central, you’ll need to add more deployment-related settings in this file.

common/pom.xml

The maven configuration file for the "common" module, which will contain most of your cn1lib’s source code, CSS files, and properties files. If your library depends on other libraries or jar files, you’ll be adding them as dependencies in this file, and not the root pom.xml file.

common/codenameone_library_appended.properties

This file is where you can specify properties that should be merged with the codenameone_settings.properties of application projects that include this library as a dependency. This is where you would add, for example, Gradle dependencies required for the Android builds, or CocoaPods dependencies that are required for iOS builds.

common/codenameone_library_required.properties

This file allows you to specific build hints that must be present in application projects that include this library. If this library requires a particular android build tools version, or a specific Java version, then those requirements should be specified in this file.

Important directories

As mentioned earlier, 99% of all your development will likely occur inside the "common" module. The other modules are for native implementations of Native interfaces.

common/src/main/java

This is where your cross-platform Java source files will be placed.

common/src/main/css

If your library uses CSS, this is where all CSS-related files will be placed.

common/src/main/resources

Other non-java resources that you want to have included in the classpath.

Building the library
Command line

To build the library, run the "install" goal on the root module as follows:

mvn install
IntelliJ IDEA

Press the "build" intellij build icon button on the toolbar.

NetBeans

Right-click on the "root" module in the project explorer and select Build.

netbeans right click build
You must build the root module and not one of the submodules.

Or you could have selected the "root" module in the project explorer and pressed the "build" netbeans build button button on the toolbar.

Eclipse IDE

Right-click on the "root" module in the project explorer and select Run as > Maven Install

eclipse build cn1lib
If the build fails for any reason, check to make sure that your project is using the latest version of the Codename One plugin. You can do this by opening the pom.xml file, and changing the cn1.version and cn1.plugin.version properties to reference the latest version. Check for the latest version, listed as <release> in the repository metadata.
Building the Legacy.cnlib file

When using the Maven build tool, you no longer require the.cn1lib file at all. Your library projects can be handled entirely via Maven’s dependency mechanism. The preferred way to distribute your libraries is on Maven central, and the preferred way to add a library to an application is via a Maven "pom" dependency.

That being said, you may still want to distribute your library as a.cn1lib file for the sake of users who are still using Ant as their build tool. For that reason, when you build a library project, the cn1lib is automatically built as well. After running a build, you can look in the common/target directory and find your.cn1lib file ready to be distributed.

Editing Java Code

To get acquainted with your project, add a "Hello World" Java class that you want to make available as part of your cn1lib.

Add a new class inside the "common/src/main/java" directory with package com.example, and name HelloWorld. Enter the following contents into the class:

public class HelloWorld {
    public static void helloWorld() {
        System.out.println("Hello world");
    }
}

Now build the library again. (See Section 2.5.1.6, “Building the library”).

Using the library in an application project

Now that you’ve built your library and added a Java class, try adding it as a dependency in an application project. If you haven’t yet created an application project, do that now. See Section 2.2.2, “Creating a new project” for instructions on creating a new application project.

Open the common/pom.xml file of your application project.

Make sure you’re editing the common/pom.xml file of the application project and not the library project.

This file may look a little hairy as there is a lot of configuration in there. You will be looking for the <dependencies> section.

The common/pom.xml file will have more than one <dependencies> tag, as it includes some profiles handling things like Kotlin support. There will be one particular <dependencies> tag that includes a comment like

<!-- INJECT DEPENDENCIES -->

You should add your dependencies before this comment.

For the sake of this example, suppose your library was set up with the following coordinates:

groupId:

com.example

artifactId:

mylib

version:

1.0-SNAPSHOT

In this case you would add the following XML snippet to the <dependencies> section of your application’s common/pom.xml file:

<dependency>
    <groupId>com.example</groupId>
    <artifactId>mylib-lib</artifactId>
    <version>1.0-SNAPSHOT</version>
    <type>pom</type>
</dependency>
Notice that you appended "-lib" to the artifactId. This is because you’re including the "lib" module of your library project as the dependency, and not the root module. Also the <type>pom</type> is important as it indicates that this is a pom dependency - not a regular jar dependency.

Now try it out. Try adding the following code to your application project’s main class (or anywhere in the application project, for that matter):

This calls into the library you just built, so it isn’t one of the compiled examples:

import com.example.HelloWorld;

// ... inside start()
HelloWorld.helloWorld();

And build the project. The project should build OK, and if you run it, you should see that the helloWorld() method works as designed.

2.5.2. Adding menus to the simulator

A cn1lib can contribute menu items to the Codename One simulator’s menu bar. This is the same affordance the framework itself uses for the Skins / Native Theme / Simulate menus — opened up so library authors can expose backend-specific actions (for example, "Add a simulated peripheral," "Inject a push notification," or "Switch backend") without users having to write any Swing code or instrument their app.

Every menu item is also reachable from CN1 UnitTests (or any app code) through CN.execute("namespace:itemN") — the JavaSE port’s URL execute is overloaded to recognize a registered hook url and dispatch it on the EDT instead of opening it as a browser URL. On Android, iOS, JavaScript and other production targets no hooks are registered, so a hook-style URL falls through to the normal native execute and (almost always) becomes a no-op; tests should pair CN.execute with CN.canExecute for that reason. Hooks can also be declared with no menu label, which makes them callable from tests but invisible in the menu — useful for state-priming actions a human wouldn’t click.

The contract

Each cn1lib ships a properties file at a well-known classpath location. The simulator scans every jar on its classpath for this resource and merges the results, so multiple cn1libs coexist cleanly:

# META-INF/codenameone/simulator-hooks.properties
name=Bluetooth
namespace=bluetooth   # optional; defaults to slugified `name`

# Each itemN is the action; the matching labelN is the menu text.
# Items are positional — the loader reads item1, item2, item3, ... and
# stops at the first missing index. Don't skip numbers.
item1=com.example.bt.simulator.Hooks#toggleAdapter
label1=Toggle adapter on/off

item2=com.example.bt.simulator.Hooks#addDemoPeripheral
label2=Add demo peripheral

# Label omitted → API-only hook. Callable from tests, invisible in menu.
item3=com.example.bt.simulator.Hooks#primeReadFailure

Required keys:

name

Menu title shown in the simulator’s menu bar. One menu per properties file.

itemN

A fully.qualified.ClassName#staticMethodName reference for the Nth menu item. The method must be public static void and take no arguments. Items are numbered from 1 upward; the loader stops at the first missing itemN, so don’t leave gaps.

Optional keys:

namespace

Identifier used for the CN.execute lookup (for example, bluetooth for a URL like bluetooth:item1). Defaults to lowercased, ASCII-slugified name (Push Notifications! → push-notifications). Set this explicitly when you want a different identifier from the display name.

labelN

Display text for the matching itemN. Omit entirely to make the hook API-only — registered with CN.execute but hidden from the menu.

No groups, no submenus, no priority — flat by design. If you need ordering relative to another cn1lib, you can’t have it: discovery order wins, and that’s intentional so the contract stays small and the future simulator UX can re-render this metadata however it likes.

The action method

The simulator dispatches every action on the Codename One EDT through Display.callSerially, so your method can call Display.getInstance(), Form.show(), Dialog.show(), ToastBar.showInfoMessage() and any other CN1 API. Reflection uses the same classloader that loaded Display, so cn1lib internals (including package-private classes) resolve normally:

This is a method in your own cn1lib, so it isn’t one of the compiled examples:

public static void resetPairing() {
    PairingState.clear();                 // package-private, same cn1lib
    if (Display.isInitialized()) {
        ToastBar.showInfoMessage("Pairing state cleared");
    }
}

The Display.isInitialized() guard is a useful pattern when the same static methods are also called from JUnit tests that don’t run inside a live CN1 simulator — the state mutation runs in both contexts, only the UI feedback is skipped.

Worked example using cn1-bluetooth

The cn1-bluetooth cn1lib ships a JavaSE port with two backends: a scriptable in-memory simulator and a real-hardware backend that talks to a native helper (CoreBluetooth on macOS, BlueZ on Linux, WinRT on Windows). Both are useful in different stages of development, and the menu lets the user choose between them and exercise the simulator without writing any test scaffolding.

Its simulator-hooks.properties looks like this:

name=Bluetooth
namespace=bluetooth

item1=com.codename1.bluetoothle.BluetoothSimulatorHooks#toggleAdapter
label1=Toggle adapter on/off

item2=com.codename1.bluetoothle.BluetoothSimulatorHooks#addDemoPeripheral
label2=Add demo peripheral

item3=com.codename1.bluetoothle.BluetoothSimulatorHooks#switchToNativeBle
label3=Switch backend → native BLE (real hardware)

# API-only: used by the test suite but never displayed in the menu
item4=com.codename1.bluetoothle.BluetoothSimulatorHooks#primeReadFailure

When the user runs an app that depends on cn1-bluetooth, the simulator’s menu bar gets a Bluetooth menu with the three labeled items. Clicking Add demo peripheral drops a peripheral into the in-memory simulator that the running app can then scan for, connect to, and exchange data with — without any real hardware. item4 is callable from tests via CN.execute("bluetooth:item4") but never shows up in the menu.

Calling hooks from CN1 unit tests

CN1 unit tests (AbstractTest subclasses run via mvn cn1:test) compile under the same restrictions as the rest of the app — no reflection, no JavaSE-only imports. Drive a hook the same way you’d execute any URL — CN.execute recognizes registered hook urls and dispatches them on the EDT:

public class BluetoothDemoTest extends AbstractTest {
    @Override
    public boolean runTest() throws Exception {
        // Skip cleanly off-simulator: a real device has no hook registered
        // and CN.canExecute will not return TRUE.
        if (!Boolean.TRUE.equals(CN.canExecute("bluetooth:item2"))) {
            return true;
        }
        // Seed the simulator — same effect as clicking "Add demo peripheral".
        CN.execute("bluetooth:item2");
        // nullnow drive the public Bluetooth API as usual.
        return true;
    }
}

A common pattern: ship label-bearing hooks for actions a developer might want to fire manually (toggle adapter, inject notification), and ship label-less hooks for test-only state setup (primeReadFailure, seedFixture) that would just clutter the menu.

What’s intentionally not exposed
  • Swing types. JMenu, JMenuItem, KeyStroke and friends don’t appear in the contract. The simulator UX may change shape (toolbar, command palette, sidebar) and cn1libs shouldn’t have to follow.

  • Submenus, separators, priority. The metadata is a flat list with no hierarchy. If you want grouping, ship multiple simulator-hooks.properties files in separate jars — each becomes its own menu.

  • Long-running work on the EDT. Hook methods run on the CN1 EDT; if you need to do I/O, fire-and-forget a new Thread(…​) from the method or use CN.invokeAndBlock so you don’t block the UI.

2.5.3. Distributing your library

The recommended way to distribute your library is on Maven central. That way users will be able to install your library by copying and pasting a familiar <dependency> snippet into their pom.xml file.

3. Gradle project workflow

Maven is the standard way to build a Codename One application, and everything in the Maven project workflow chapter remains the reference. Gradle is an optional alternative for applications that want a smaller project: one directory instead of a root pom.xml and eight modules, and no native or backend directories until the application has native code or a server.

The Gradle build isn’t a second implementation. The com.codenameone Gradle plugin calls the same build engine the Maven plugin uses, so a Gradle project stages the same upload for the build server, runs the same local builders, compiles CSS with the same compiler and checks the same bytecode rules. An application built both ways uploads identical content for every target.

3.1. Choosing between Maven and Gradle

Choose Gradle when:

  • You want the smallest project that works: a settings file, your sources and nothing else.

  • Your team already uses Gradle, or your IDE and CI work better with it.

  • The application doesn’t need any of the Maven-only goals listed in Section 3.10, “Limits”.

Stay with Maven when:

  • The application targets Java 8. Gradle projects compile for Java 17 only, the bytecode level Codename One builds for every platform.

  • It still uses legacy .cn1lib files rather than libraries published to a Maven repository.

  • It depends on a goal that has no Gradle task yet.

Gradle projects leave out what the Maven build carries for older projects: legacy .cn1lib files (consumed or produced), the legacy Designer (resource editor) task, the Java 8 target, and the Maven-only files and profiles such as codenameone_maven.properties and the install-codenameone profile. Running Gradle needs JDK 17 or newer and Gradle 8.5 or newer; every project ships a Gradle wrapper, so you never install Gradle yourself.

3.2. Getting started

3.2.1. The initializr

In the Codename One initializr, set Build tool to Gradle. A Gradle project then offers three project types:

App

A client application.

App + backend

A client application with a backend/ subproject, as described in Section 3.8, “The backend”.

Backend only

A server with no client at all: no settings file, theme, simulator or native directories.

The download opens in IntelliJ IDEA, NetBeans, Eclipse or Visual Studio Code, and builds from the command line with the ./gradlew wrapper it contains.

3.2.2. Converting an existing project

The Maven plugin converts a Maven or Ant application to the Gradle layout, writing a new project beside the old one and leaving the old one untouched. In the commands in this chapter, VERSION stands for the Codename One release you use:

cd MyApp          # the Maven or Ant project
mvn com.codenameone:codenameone-maven-plugin:VERSION:convert-to-gradle
cd ../MyApp-gradle
./gradlew run

The conversion runs anywhere, with or without a project in the working directory. These properties control it:

mvn com.codenameone:codenameone-maven-plugin:VERSION:convert-to-gradle \
  -Dcn1.sourceProject=/path/to/MyApp \
  -Dcn1.outputDir=/path/to/MyApp-gradle \
  -Dcn1.includeBackend=true
cn1.sourceProject

The project to convert. Defaults to the project Maven runs in, else the working directory.

cn1.outputDir

Where the Gradle project goes. Defaults to <project>-gradle beside the source project. It must not exist or must be empty.

cn1.includeBackend

Carries over a backend module that’s still the untouched archetype skeleton. Off by default, because every Maven project has that module; a backend with code of its own is always converted.

cn1.gradleVersion

The plugin version the new settings.gradle.kts declares. Defaults to the version of the Maven plugin doing the conversion.

The conversion moves sources, CSS, localization and resources to src/main/…​, each platform’s native implementations to src/<platform>/<lang>, and the dependencies of common/pom.xml into build.gradle.kts. It raises codename1.arg.java.version to 17 and says so. Ant projects that mix Java and Kotlin in src/ get their Kotlin sources in src/main/kotlin and the Kotlin Gradle plugin in build.gradle.kts.

A project that uses legacy .cn1lib files is refused, and the message names every one of them. Depend on each library’s Maven coordinates instead, or republish it as a Gradle cn1lib (see Section 3.6, “Libraries”), then convert again.

The generate-app-project goal, which migrates an Ant project or a project template, produces a Gradle project directly with -Dcn1.buildTool=gradle:

mvn com.codenameone:codenameone-maven-plugin:VERSION:generate-app-project \
  -DgroupId=com.example.myapp \
  -DartifactId=myapp \
  -Dversion=1.0-SNAPSHOT \
  -DmainName=MyApp \
  -DsourceProject=/path/to/AntProject \
  -Dcn1.buildTool=gradle \
  -DinteractiveMode=false

3.3. The project layout

MyApp/
  settings.gradle.kts
  build.gradle.kts            (optional: the application's own dependencies)
  gradle.properties
  gradlew, gradlew.bat, gradle/wrapper/
  codenameone_settings.properties
  icon.png
  src/main/java/              (and src/main/kotlin/ for Kotlin sources)
  src/main/css/theme.css
  src/main/resources/
  src/main/l10n/
  src/test/java/
  src/android/java/           created by generateNativeInterfaces, when needed
  src/ios/objectivec/
  src/javase/java/
  src/javascript/javascript/
  src/win/c/
  src/linux/c/
  backend/                    created by addBackend, when needed

The native and backend directories don’t exist until the application needs them. The plugin declares them anyway, so creating one later needs no build-file edit.

The settings file is the only Codename One line a project needs, with VERSION replaced by the release:

pluginManagement {
    repositories {
        maven("https://repo.codenameone.com/maven2")
        gradlePluginPortal()
    }
}

plugins {
    id("com.codenameone") version "VERSION"
}

rootProject.name = "MyApp"

The plugin, applied from the settings file, adds the repository the framework is published to (and Maven Central for other libraries), applies itself to the project and to backend/ when there is one, and builds against its own version. There’s one version number to bump.

A project’s kind comes from the files the other build tools already use: codenameone_settings.properties makes it an application, codenameone_library_appended.properties a cn1lib, and application.properties without a settings file a backend. The codename1.kind Gradle property (APP, LIB or BACKEND) overrides the detection for the root project. Subprojects, such as an application’s backend, are always detected from their own files, because a Gradle property set on the command line or in gradle.properties reaches every project of the build.

The generated gradle.properties turns on the configuration cache and the build cache, which is what makes a second ./gradlew run start in about a second:

# Gradle caches the configuration of this build, which is what makes the second
# ./gradlew run start in about a second.
org.gradle.configuration-cache=true
org.gradle.caching=true

To resolve the framework from a mirror, or from a directory holding a locally built Codename One, set codename1.repository there:

# Resolve the plugin's framework artifacts from a mirror, or from a local
# directory holding a locally built Codename One, instead of repo.codenameone.com.
codename1.repository=/path/to/maven/repository

3.4. Running and building

./gradlew run      # the simulator
./gradlew debug    # the simulator, waiting for a debugger on port 5005
./gradlew cn1Test  # the unit tests, in the simulator's test runner

The simulator reloads CSS as you edit it, and its hot reload recompiles through Gradle. Cloud builds and local builds are tasks too:

./gradlew buildAndroid           # send an Android build to the build server
./gradlew buildIos               # send an iOS debug build
./gradlew buildIosXcodeProject   # generate an Xcode project locally
./gradlew buildJavascriptLocal   # build the JavaScript port locally

cn1Build runs any target by name, and codename1.stageOnly stops once the upload jar is staged and checked, which is useful for inspecting exactly what a build would send:

./gradlew cn1Build -Pcodename1.platform=ios -Pcodename1.buildTarget=ios-source
./gradlew buildAndroid -Pcodename1.stageOnly=true

Gradle runs one Codename One build at a time, even when a command line names several, because each one uses the build client and the local toolchains.

3.4.1. Task reference

Gradle taskMaven goalWhat it does

run

cn1:run

Runs the application in the simulator.

debug

cn1:debug

Runs the simulator suspended, waiting for a debugger on port 5005.

cn1Css

cn1:css

Compiles src/main/css/theme.css, merged with the CSS of every cn1lib, into theme.res.

transcodeSvg

cn1:transcode-svg

Turns SVG and Lottie assets into Java sources.

generateGuiSources

cn1:generate-gui-sources

Generates sources from GUI builder XML and CodeRAD view templates.

cn1Compile

compile

Compiles the application and its simulator native code; the simulator’s hot reload runs it.

prepareSimulator

cn1:prepare-simulator-classpath

Writes the files the simulator reads at startup.

cn1Test

cn1:test

Runs the unit tests in the simulator’s test runner.

generateNativeInterfaces

cn1:generate-native-interfaces

Writes implementation stubs for every native interface; see Section 3.5, “Native interfaces”.

verifyNativeInterfaces<Build>

(none)

Runs before each build task and fails when a native interface has no implementation for that platform.

buildAndroid, buildAndroidGradleProject

cn1:buildAndroid, cn1:buildAndroidGradleProject

An Android cloud build; an Android Studio project generated locally.

buildIos, buildIosRelease, buildIosXcodeProject

cn1:buildIos, cn1:buildIosRelease, cn1:buildIosXcodeProject

An iOS debug or App Store cloud build; an Xcode project generated locally.

buildMacNative, buildMacDesktop

cn1:buildMacNative, cn1:buildMacDesktop

A native macOS build; a macOS desktop (JVM) build.

buildWindowsDesktop, buildWindowsDevice

cn1:buildWindowsDesktop, cn1:buildWindowsDevice

A Windows desktop (JVM) build; a native Windows build.

buildLinuxDevice

cn1:buildLinuxDevice

A native Linux build.

buildJavascript, buildJavascriptLocal

cn1:buildJavascript

A JavaScript cloud build; the JavaScript port built locally.

cn1Build

cn1:build

The build given by -Pcodename1.platform and -Pcodename1.buildTarget.

settings, guibuilder, gameBuilder, certificateWizard

cn1:settings, cn1:guibuilder, cn1:gamebuilder, cn1:certificatewizard

Opens Codename One Settings, the GUI Builder, the Game Builder or the iOS Certificate Wizard for this project.

addBackend

(none)

Adds a backend/ subproject to the application.

runBackend

cn1:backend

Runs a backend on this JVM.

backendPackage

cn1:backend-package

Builds a backend as a single native binary.

backendTest

test -Dcn1.backend.compiledTests=true

Runs a backend’s tests as a native binary. The standard test task runs them on this JVM.

cn1Update

cn1:update

Updates the plugin version in settings.gradle.kts and the build client.

A build task accepts -Pautomated=true to wait for a cloud build and download its result, and -Popen=false (or -Pcodename1.open=false) to keep a generated Xcode or Android Studio project from opening.

3.4.2. Build hints

Build hints come from codenameone_settings.properties, as in every Codename One project. The codenameone {} block in build.gradle.kts adds hints for every build of the project, without the codename1.arg. prefix:

codenameone {
    buildHints.put("ios.newStorageLocation", "true")
    buildHints.put("android.targetSDKVersion", "35")
}

A hint on the command line applies to that build only, and wins over both. -Pcodename1.arg.<hint> and -Dcodename1.arg.<hint> are equivalent:

./gradlew buildIos -Pcodename1.arg.ios.newStorageLocation=true

The same block also sets version, the framework version the project builds against (it defaults to the plugin’s), and mainClass, which defaults to the package and main name in the settings file.

3.5. Native interfaces

A Gradle project starts without native directories, and adding a native interface later needs no change to any build file.

  1. Declare the interface in src/main/java, as described in native interfaces:

    public interface MyNative extends NativeInterface {
        String helloWorld(String hi);
    }
  2. Generate the stubs:

    ./gradlew generateNativeInterfaces

    The task compiles the application, finds every interface that extends NativeInterface, and writes a stub for each platform into src/<platform>/<lang>, creating only the directories it writes to. An existing implementation is never overwritten unless you pass -Pcn1.overwrite=true.

  3. Implement the stubs. The Java SE implementation in src/javase/java compiles against the application and joins the simulator’s classpath on the next ./gradlew run. The other platforms' sources travel in that platform’s upload.

-Pcn1.nativeInterface limits the task to one interface, and -Pcn1.swift=true and -Pcn1.kotlin=true add Swift and Kotlin stubs beside the Objective-C and Java ones:

./gradlew generateNativeInterfaces -Pcn1.nativeInterface=com.example.myapp.MyNative \
  -Pcn1.swift=true -Pcn1.kotlin=true

Before every build, the plugin checks that each native interface has an implementation for the target platform. A missing one fails the build before anything is sent, naming the file to create, rather than failing later on the build server.

To remove a native interface, delete the interface and its implementation files; nothing else refers to them.

3.6. Libraries

3.6.1. Using a cn1lib

Declare a cn1lib by its -lib coordinates in build.gradle.kts, next to any plain Java library:

dependencies {
    // A Codename One library published to a Maven repository:
    cn1lib("com.codenameone:googlemaps-lib:1.0")

    // A plain Java library that only uses APIs Codename One supports:
    implementation("org.example:library:1.0")
}

These are the coordinates a Maven project uses with <type>pom</type>. The library’s classes and CSS reach every build, and its platform-specific code reaches only that platform’s: Maven chooses those jars with profiles, and the plugin reads the same profiles from the library’s published pom.xml. The library’s build hints merge into the application’s, as they do under Maven.

A project that declares repositories of its own in build.gradle.kts keeps the Codename One repository too: the plugin adds it beside them.

3.6.2. Building a cn1lib

A Gradle project with a codenameone_library_appended.properties file (and no codenameone_settings.properties) is a cn1lib. It uses the same settings file, src/main/java for the library, src/main/css for its CSS and src/<platform>/<lang> for native code, and generateNativeInterfaces works as it does for an application. The build script sets the coordinates and, to publish somewhere other than the local repository, a repository:

group = "com.example"
version = "1.0"

publishing {
    repositories {
        maven {
            name = "company"
            url = uri("https://maven.example.com/releases")
        }
    }
}
./gradlew publishToMavenLocal   # install into ~/.m2 for local testing
./gradlew publish               # publish to the repositories the build declares

The library is published in the same shape the Maven cn1lib-archetype produces, so Maven and Gradle applications both consume it. For a project named N:

N-common

The compiled library, with codenameone_library_appended.properties and codenameone_library_required.properties under META-INF/codenameone/.

N-common, classifier cn1css

The library’s CSS, as a zip.

N-<platform>

One jar per platform: compiled classes for javase, sources for the others, which the native builders compile.

N-lib

A pom that applications depend on, with one codename1.platform profile per platform.

Gradle cn1libs don’t produce .cn1lib files.

3.7. Kotlin

Kotlin sources go in src/main/kotlin, and the project applies the Kotlin Gradle plugin in build.gradle.kts. plugins {} must be the first statement of the script:

plugins {
    kotlin("jvm") version "2.2.10"
}

dependencies {
    // the application's own dependencies, as before
}

Java and Kotlin classes in one project can call each other. Both are checked by the same bytecode compliance rules as a Java-only project.

3.8. The backend

addBackend adds a backend/ subproject to an existing application. The settings plugin includes it automatically, so settings.gradle.kts doesn’t change. Its tasks run with the project path in front:

./gradlew addBackend                          # create backend/ once
CN1_PROFILE=dev ./gradlew :backend:runBackend  # run it on this JVM
./gradlew :backend:backendPackage              # build the native binary
./gradlew :backend:test                        # run its tests on this JVM
./gradlew :backend:backendTest                 # and as a native binary

A backend-only project has the same bootstrap with the server at the root: application.properties, application-dev.properties and src/main/java. Its tasks are runBackend, backendPackage, backendTest, cn1Update and the standard test, which runs the backend’s tests on this JVM; Chapter 35, Backend testing covers writing them. backendPackage writes the binary to build/<project name>:

CN1_PROFILE=dev ./gradlew runBackend
./gradlew backendPackage
CN1_PROFILE=dev PORT=9000 ./build/MyService

CN1_PROFILE=dev selects the development profile that application-dev.properties configures, with an in-memory SQLite database; without it the server reads the production settings and expects DATABASE_URL. PORT overrides the configured port. runBackend takes -Pcn1.backend.mainClass, -Pcn1.backend.args and -Pcn1.backend.jvmArgs. The server-side backend chapter covers the runtime itself.

3.9. Tools and updates

The desktop tools open on the Gradle project as they do on a Maven one:

./gradlew settings            # Codename One Settings
./gradlew guibuilder          # the GUI Builder
./gradlew gameBuilder         # the Game Builder
./gradlew certificateWizard   # the iOS Certificate Wizard

cn1Update moves the project to the latest release by rewriting the plugin version in settings.gradle.kts, and updates the build client. -Pcodename1.updateTo picks a version instead:

./gradlew cn1Update
./gradlew cn1Update -Pcodename1.updateTo=VERSION

3.10. Limits

These features remain Maven goals, with no Gradle task yet:

  • The OpenAPI, gRPC and GraphQL client generators (cn1:generate-openapi, cn1:generate-grpc, cn1:generate-graphql).

  • cn1:create-game-scene.

  • The on-device debugging helpers for Android and iOS.

The initializr’s Tweet app template isn’t available for Gradle projects yet. An application that needs one of these stays on Maven for now.

Core concepts

4. The EDT - Event Dispatch Thread

4.1. What’s The EDT

Codename One allows developers to create as many threads as they want; however to interact with the Codename One user interface components a developer must use the EDT. The EDT stands for "Event Dispatch Thread" but it handles a lot more than "events."

The EDT is the main thread of Codename One, by using one thread Codename One can avoid complex synchronization code and focus on simple functionality that assumes one thread.

This has huge advantages for your code. You can normally assume that all code will occur on a single thread and avoid complex synchronization logic.

You can visualize the EDT as a loop such as this:

while(codenameOneRunning) {
     performEventCallbacks();
     performCallSeriallyCalls();
     drawGraphicsAndAnimations();
     sleepUntilNextEDTCycle();
}

Normally, every call you receive from Codename One will occur on the EDT. For example, every event, calls to paint(), and lifecycle calls (start etc.) should all occur on the EDT.

This is pretty powerful, however it means that as long as your code is processing nothing else can happen in Codename One!

If your code takes too long to execute then no painting or event processing will occur during that time, so a call to Thread.sleep() will actually stop everything!

The solution is pretty simple, if you need to perform something that requires intensive CPU you can spawn a thread.

Codename One’s networking code automatically spawns its own network thread (see the NetworkManager). However, this also poses a problem…​

Codename One assumes all modifications to the UI are performed on the EDT. If you spawn a separate thread, how do you force its modifications back into the EDT?

Codename One includes helper methods in the Display class to help in these situations: isEDT(), callSerially(Runnable), callSeriallyOnIdle(Runnable), and callSeriallyAndWait(Runnable) (with an optional timeout overload).

isEDT() is useful for generic code that needs to test whether the current code is executing on the EDT.

The two methods that coordinate work with the EDT pull in opposite directions, and the difference is easiest to see against the cycle itself. callSerially moves a Runnable onto the EDT. invokeAndBlock moves one the other way: called on the EDT it hands the Runnable to a pooled thread, and called off the EDT it simply runs it on the calling thread. Either way that Runnable isn’t on the EDT, so don’t touch the UI from inside it:

The EDT cycle and how callSerially and invokeAndBlock hand work to it
Figure 12. The EDT’s cycle, and where callSerially and invokeAndBlock enter it

4.2. Call Serially (And Wait)

callSerially(Runnable) should normally be called off the EDT (in a separate thread), the run method within the submitted runnable will be invoked on the EDT.

The Runnable passed to the callSerially and callSeriallyAndWait methods isn’t a Thread. Use the Runnable interface as a convenient callback interface:
// this code is executing in a separate thread
final String res = methodThatTakesALongTime();
Display.getInstance().callSerially(new Runnable() {
     public void run() {
          // this occurs on the EDT so I can make changes to UI components
          resultLabel.setText(res);
     }
});
You can write this code more concisely using Java 8 lambda code as such:
// this code is executing in a separate thread
String res = methodThatTakesALongTime();
Display.getInstance().callSerially(() -> resultLabel.setText(res));

This allows code to leave the EDT and then later on return to it to perform things within the EDT.

The callSeriallyAndWait(Runnable) method blocks the current thread until the method completes, this is useful for cases such as user notification e.g.:

// this code is executing in a separate thread
methodThatTakesALongTime();
Display.getInstance().callSeriallyAndWait(() -> {
  // this occurs on the EDT so I can make changes to UI components
  globalFlag = Dialog.show("Are You Sure?", "Do you want to continue?", "Continue", "Stop");
});
// this code is executing the separate thread
// global flag was already set by the call above
if(!globalFlag) {
   return;
}
otherMethod();
If you’re unsure use callSerially. The use cases for callSeriallyAndWait are rare. When you do need to wait, the overload that accepts a timeout lets background threads marshal results back to the EDT without risking an indefinite stall if the EDT is busy.

If the work you’re posting back to the EDT is expensive and can wait until the UI finishes its current pass, use callSeriallyOnIdle(). This queues the runnable for execution after the EDT completes painting, animations, and any other queued tasks. Deferring longer tasks to the idle queue helps avoid starving animations and transitions when bursts of background callbacks arrive together.

4.2.1. callSerially On The EDT

One of the misunderstood topics is why you’d ever want to invoke callSerially when you’re still on the EDT. This is best explained by example. Say you have a button that has quite a bit of functionality tied to its events e.g.:

  1. A user added an action listener to show a Dialog.

  2. A framework the user installed added some logging to the button.

  3. The button repaints a release animation as its being released.

However, this might cause a problem if the first event being handled (the dialog) causes an issue with the following events. For example, a dialog will block the EDT (using invokeAndBlock), events will keep happening but since the event you’re in "already happened" the button repaint and the framework logging won’t occur. The same problem also occurs if you show a form which might trigger logic that relies on the current form still being present.

One of the solutions to this problem is to wrap the action listeners body with a callSerially. In this case the callSerially will postpone the event to the next cycle (loop) of the EDT and let the other events in the chain complete. Notice that you shouldn’t use this normally since it includes an overhead and complicates application flow, however when you run into issues in event processing this is worth trying to see if its the cause.

You should never invoke callSeriallyAndWait on the EDT since this would effectively mean sleeping on the EDT. That method throws an exception if its invoked from the EDT.

If you need to run logic on the EDT but must ensure nothing inside it blocks, the Display class also provides invokeWithoutBlocking() and invokeWithoutBlockingWithResultSync(). These helpers temporarily disable invokeAndBlock() during the runnable. Any nested call to invokeAndBlock() while blocking is disabled results in a BlockingDisallowedException. Framework code that wraps user callbacks can use these methods to guard against accidental nested blocking that would otherwise deadlock or stall the UI, yet still obtain a return value when needed via invokeWithoutBlockingWithResultSync().

4.3. Debugging EDT Violations

Two types of EDT violations exist:

  1. Blocking the EDT thread so the UI performance is considerably slower.

  2. Invoking UI code on a separate thread

Codename One provides a tool to help you detect some of these violations some caveats may apply though…

It’s an imperfect tool. It might fire "false positives" meaning it might detect a violation for legal code and it might miss some illegal calls. However, it’s a valuable tool in the process of detecting hard to track bugs that are sometimes reproducible on the devices (due to race condition behavior).

To activate this tool select the Debug EDT menu option in the simulator and pick the level of output you wish to receive:

Debug EDT
Figure 13. Debug EDT

Full output will include stack traces to the area in the code that’s suspected in the violation.

4.4. Invoke And Block

Invoke and block is the exact opposite of callSeriallyAndWait(), it blocks the EDT and opens a separate thread for the runnable call. This functionality is inspired by the Foxtrot API, which is a remarkably powerful tool most Swing developers don’t know about.

This is best explained by an example. Typical Java code reads as a single sequence as such:

doOperationA();
doOperationB();
doOperationC();

This works well normally but on the EDT it might be a problem, if one of the operations is slow it might slow the whole EDT (painting, event processing etc.). Normally you can move operations into a separate thread e.g.:

This means that operation C will happen in parallel to operation B which might be a problem…​
For example, instead of using operation names lets use a more "real world" example:

updateUIToLoadingStatus();
readAndParseFile();
updateUIWithContentOfFile();

Notice that the first and last operations must be conducted on the EDT but the middle operation might be slow! Since updateUIWithContentOfFile needs readAndParseFile to occur before it starts doing the new thread won’t be enough.

A simplistic approach is to do something like this:

updateUIToLoadingStatus();
new Thread() {
    public void run() {
          readAndParseFile();
          updateUIWithContentOfFile();
    }
}.start();

However, updateUIWithContentOfFile should be executed on the EDT and not on a random thread. The right way to do this would therefore be something like this:

updateUIToLoadingStatus();
new Thread() {
    public void run() {
          readAndParseFile();
          Display.getInstance().callSerially(new Runnable() {
               public void run() {
                     updateUIWithContentOfFile();
               }
          });
    }
}.start();

This is legal and would work reasonably well, however it gets complicated as you add more and more features that need to be chained serially after all these are 3 methods!

Invoke and block solves this in a unique way you can get almost the exact same behavior by using this:

updateUIToLoadingStatus();
Display.getInstance().invokeAndBlock(new Runnable() {
    public void run() {
          readAndParseFile();
    }
});
updateUIWithContentOfFile();

Or this with Java 8 syntax:

updateUIToLoadingStatus();
Display.getInstance().invokeAndBlock(() -> readAndParseFile());
updateUIWithContentOfFile();

Invoke and block effectively blocks the current EDT in a legal way. It spawns a separate thread that runs the run() method and when that run method completes it goes back to the EDT.

All events and EDT behavior still work while invokeAndBlock is running, this is because invokeAndBlock() keeps calling the main thread loop internally.

Notice that invokeAndBlock comes at a slight performance penalty. Also notice that nesting invokeAndBlock calls (or over using them) isn’t recommended.
However, they’re convenient when working with multiple threads/UI.

Even if you never call invokeAndBlock directly you’re probably using it indirectly in APIs such as Dialog that show a dialog while blocking the current thread e.g.:

public void actionPerformed(ActionEvent ev) {
  // will return true if the user clicks "OK"
  if(!Dialog.show("Question", "How Are You", "OK", "Not OK")) {
  // ask what went wrongnull
  }
}

Notice that the dialog show method will block the calling thread until the user clicks OK or Not OK…​

Other APIs such as NetworkManager.addToQueueAndWait() also make use of this feature. Pretty much every "AndWait" method or blocking method uses this API internally!

To explain how invokeAndBlock works you can return to the sample above of how the EDT works:

while(codenameOneRunning) {
     performEventCallbacks();
     performCallSeriallyCalls();
     drawGraphicsAndAnimations();
     sleepUntilNextEDTCycle();
}

invokeAndBlock() works in a similar way to this pseudo-code:

void invokeAndBlock(Runnable r) {
    openThreadForR(r);
    while(r is still running) {
         performEventCallbacks();
         performCallSeriallyCalls();
         drawGraphicsAndAnimations();
         sleepUntilNextEDTCycle();
    }
}

The EDT is effectively "blocked" but invokeAndBlock "redoes" it from within…​

As you can see this is a simple approach for thread programming in UI, you don’t need to block your flow and track the UI thread. You can program in a way that seems sequential (top to bottom) but uses multi-threading correctly without blocking the EDT.

5. Events

Most events in Codename One are routed via the high-level events (for example, action listener) but sometimes you need access to low-level events (for example, when drawing via Graphics) that provide more fine grained access. Typically working with the higher level events is far more potable since it might map to different functionality on different devices.

5.1. High-level events

High-level events are broadcast using an addListener/setListener - publish/subscribe system. Most of them are channeled via the EventDispatcher class which further simplifies that and makes sending events far easier.

All events are fired on the Event Dispatch Thread, the EventDispatcher makes sure of that.

5.1.1. Chain of events

Since all events fire on the EDT some complexities occur. E.g.:

You have two listeners monitoring the same event (or related events for example, pointer event and button click event both of which will fire when the button is touched).

When the event occurs you can run into a scenario like this:

  1. First event fires

  2. It shows a blocking dialog or invokes an "AndWait" API

  3. Second event fires after the dialog was dismissed!

This happens because events are processed in-order per cycle. Since the old EDT cycle is stuck (because of the Dialog) the rest of the events within the cycle can’t complete. New events are in a new EDT cycle so they can finish fine!

A workaround to this issue is to wrap the code in a callSerially, you shouldn’t do this universally as this can create a case of shifting the problem to the next EDT cycle. But, using callSerially will allow the current cycle to flush which should help.

Another workaround for the issue is avoiding blocking calls within an event chain.

Why a blocking call in the first listener delays the second one
Figure 14. One batch, walked in order

5.1.2. Action events

The most common high-level event is the ActionListener which allows binding a generic action event to pretty much anything. This is so ubiquitous in Codename One that it’s even used for networking (as a base class) and for some low-level event options.

For example, you can bind an event callback for a Button by using:

Button b = new Button("Click Me");
b.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent ev) {
        // button was clicked, you can do anything you want herenull
    }
});

Or thanks to the Java 8 lambdas you can write it as:

Button b = new Button("Click Me");
b.addActionListener((ev) -> {
    // button was clicked, you can do anything you want here.
});

Notice that the click will work whether the button was touched using a mouse, finger or keypad shortcut seamlessly with an action listener. Many components work with action events for example, buttons, text components, slider etc.

Quite a few high-level event types exist that are more specific to requirements.

Types of action events

When an action event is fired it’s given a type, but this type might change as the event evolves for example, a command triggered by a pointer event won’t include details of the original pointer event.

You can get the event type from getEventType(), this also gives you a rather exhaustive list of the possible event types for the action event.

Modern gesture components also publish custom action types. For example, SwipeableContainer dispatches ActionEvent.Type.Swipe when the top component is fully opened, allowing code to react to swipe gestures without monitoring low-level drags. Listening for these higher level events keeps gesture handling portable across touch and desktop targets.

Source of event

ActionEvent has a source object, what that source is depends on the event type. For most component based events this is the component but there are some nuances.

The getComponent() method might not get the actual component. In case of a lead component such as MultiButton the underlying Button will be returned and not the MultiButton itself.

To get the component that you would logically think of as the source component use the getActualComponent() method.

Event consumption

An ActionEvent can be consumed, once consumed it will no longer proceed down the chain of event processing. This is useful for some cases where you would like to block behavior from proceeding down the path.

For example, the event dispatch thread allows you to listen to errors on the EDT using:

Display.getInstance().addEdtErrorHandler((e) -> {
    Exception err = (Exception)e.getSource();
    // null
});

This will work great but you will still get the default error message from the EDT over that exception. To prevent the event from proceeding to the default error handling you can do this:

Display.getInstance().addEdtErrorHandler((e) -> {
    e.consume();
    Exception err = (Exception)e.getSource();
    // null
});

Notice that you can check if an event was already consumed using the isConsumed() method but it’s pretty unlikely that you will receive a consumed event as the system will stop sending it.

NetworkEvent

NetworkEvent is a subclass of ActionEvent that’s passed to actionPerformed callbacks made in relation to generic network code. For example:

NetworkManager.getInstance().addErrorListener(new ActionListener<NetworkEvent>() {
    public void actionPerformed(NetworkEvent ev) {
        // now we have access to the methods on NetworkEvent that provide more information about the network specific flags
    }
});

Or with Java 8 lambdas:

NetworkManager.getInstance().addErrorListener((ev) -> {
    // now we have access to the methods on NetworkEvent that provide more information about the network specific flags
});

The NetworkEvent allows the networking code to reuse the EventDispatcher infrastructure and to simplify event firing through the EDT. But you should notice that some code might not be equivalent for example, you could do this to read the input stream:

ConnectionRequest r = new ConnectionRequest() {
    @Override
    protected void readResponse(InputStream input) throws IOException {
        // read the input stream
    }
};

Or you can do something similar using this code:

ConnectionRequest r = new ConnectionRequest();
r.addResponseListener((e) -> {
    byte[] data = (byte[])e.getMetaData();
    // work with the byte data
});

These seem similar but they have one important distinction. The latter code is invoked on the EDT, so if data is big it might slow down processing. The ConnectionRequest is invoked on the network thread and so can process any amount of data without slowing down the UI.

MessageEvent and the Cross-Platform message bus

Bridging to native code often means passing messages between Codename One Java and platform specific code. The cross-platform message bus provides a high-level abstraction for that by letting you post messages to the native layer and subscribe for messages that originate there. Use Display.postMessage() to send an MessageEvent to the platform, and Display.addMessageListener() to receive events that arrive from native code, JavaScript bridges, or background services. Message events include helpers like isPromptForAudioRecorder(), isPromptForAudioPlayer(), and getPromptPromise() that allow you to hook into permission prompts emitted by the JavaScript port so custom UI can respond to platform requests while keeping the event dispatch on the EDT. This API complements NetworkEvent by covering native-to-Java messaging without requiring direct low-level callbacks.

5.1.3. DataChangedListener

The DataChangedListener is used in several places to show that the underlying model data has changed:

  • TextField - the text field provides an action listener but that "fires" when the data input is complete. DataChangedListener fires with every key entered and thus allows functionality such as "auto complete" and is indeed used internally in the Codename One AutoCompleteTextField.

  • TableModel & ListModel - the model for the Table class notifies the view that its content has changed via this event, thus allowing the UI to refresh.

There is an exhaustive example of search that’s implemented using the DataChangedListener in the Toolbar section.

5.1.4. FocusListener

The focus listener allows you to track the "selected" or focused component. It isn’t as useful as it used to be in feature phones.

You can bind a focus listener to the Component itself and receive an event when it gained focus, or you can bind the listener to the Form and receive events for every focus change event within the hierarchy.

5.1.5. ScrollListener

ScrollListener allows tracking scroll events so UI elements can be adapted if necessary.

Scrolling is seamless and this event isn’t necessary, but if developers wish to "shrink" or "fade" an element on scrolling this interface can be used to achieve that. Notice that you should bind the scroll listener to the actual scrollable component and not to an arbitrary component.

For example, in this code from the Flickr demo the Toolbar is faded based on scroll position:

public class CustomToolbar extends Toolbar implements ScrollListener {
    private int alpha;

    public CustomToolbar() {
    }

    public void paintComponentBackground(Graphics g) {
        int a = g.getAlpha();
        g.setAlpha(alpha);
        super.paintComponentBackground(g);
        g.setAlpha(a);
    }

    public void scrollChanged(int scrollX, int scrollY, int oldscrollX, int oldscrollY) {
        alpha = scrollY;
        alpha = Math.max(alpha, 0);
        alpha = Math.min(alpha, 255);
    }
}
There is a better way of implementing this exact effect using title animations illustrated here.

5.1.6. SelectionListener

The SelectionListener event is used to broadcast list model selection changes to the list view. Since list supports the ActionListener event callback its the better option since it’s more coarse grained.

SelectionListener gets fired too often for events and that might result in a performance penalty. When running on non-touch devices list selection could be changed with the keypad and a specific fire button click would fire the action event, for those cases SelectionListener made a lot of sense. But, in touch devices this API isn’t as useful.

5.1.7. StyleListener

StyleListener allows components to track changes to the style objects. For example, if the developer does something like:

cmp.getUnselectedStyle().setFgColor(0xffffff);

This will trigger a style event that will eventually lead to the component being repainted. This is quite important for the component class but not an important event for general user code. It’s recommended that developers don’t bind a style listener.

5.1.8. Component state change events

Component instances now publish lifecycle hooks that fire when they become initialized on a form and when they’re removed. You can subscribe with Component.addStateChangeListener() to receive ComponentStateChangeEvent instances that show whether the component is transitioning to the initialized state. This is useful for running setup or teardown logic alongside focus, scroll, and selection listeners.

5.1.9. Event dispatcher

When creating your own components and objects you sometimes want to broadcast your own events, for that purpose Codename One has the EventDispatcher class which saves a lot of coding effort in this regard. For example, if you wish to provide an ActionListener event for components you can add this to your class:

private final EventDispatcher listeners = new EventDispatcher();

public void addActionListener(ActionListener a) {
    listeners.addListener(a);
}
public void removeActionListener(ActionListener a) {
    listeners.removeListener(a);
}

Then when you need to broadcast the event use:

private void fireEvent(ActionEvent ev) {
    listeners.fireActionEvent(ev);
}

5.2. Low-level events

Low-level events map to "system" events directly. Touch events are considered low-level since they might expose platform specific nuances to your code.

For example, one platform might send many events during drag while another might send a few. The high-level event handling hides those complexities but some of them trickle down into the low-level event handling.

Codename One tries to hide some complexities from the low-level events as well. But, due to the nature of the event types it’s a more challenging task.

Low-level events can be bound in one of 3 ways:

  • Use one of the add listener methods in Form for example, addPointerPressedListener.

  • Override one of the event callbacks on Form

  • Override one of the event callbacks on a Component.

That last one depends on the event. A key callback on an ordinary Component only fires while that component holds focus, because Form routes key events to the focused one - the exception is a key the menu bar claims, which Form.keyPressed hands to MenuBar first. That exception has its own exception: focusedHandlesInput gives a printable key back to the focused component when it consumes raw text input, which is how a focused text editor keeps typing out of the menu bar.

A pointer callback has no focus prerequisite at all, and three things about its target are worth knowing. Form.pointerPressed resolves the lead parent of whatever sits under the touch, so isEnabled() and isFocusable() are asked of that parent rather than of the component you pressed. The callback itself goes to the lead component, which is the class to override or attach a listener to. And focus is assigned only when the lead parent is focusable and enabled and the press isn’t a scroll wheel - in the content pane both before the callback and again after it, so moving focus from inside a pointer callback there can be undone the moment it returns.

The path a pointer event takes from the native callback to your listener
Figure 15. How one touch reaches your code

Each of those has advantages and disadvantages, specifically:

  • 'Form' based events and callbacks deliver pointer events in the 'Form' coordinate space.

  • 'Component' based key events require focus, pointer events don’t

  • 'Form' based events can block existing functionality from proceeding through the event chain for example, you can avoid calling super in a form event and thus block other events from happening (for example, block a listener or component event from triggering).

Table 1. Event type map

Listener

Override Form

Override Component

Coordinate System

Form

Form

Component

Block current functionality

Yes, just avoid super

Partially (event consume)

No

5.2.1. Low-level event types

There are two basic types of low-level events: Key and Pointer.

Key events are relevant to physical keys and won’t trigger on virtual keyboard keys, to track those use a TextField with a DataChangedListener as mentioned above.

The pointer events (touch events) can be intercepted by overriding one or more of these methods in Component or Form. Notice that unless you want to block functionality you should probably invoke super when overriding:

public void pointerDragged(int[] x, int[] y)
public void pointerDragged(final int x, final int y)
public void pointerPressed(int[] x, int[] y)
public void pointerPressed(int x, int y)
public void pointerReleased(int[] x, int[] y)
public void pointerReleased(int x, int y)
public void pointerHover(int[] x, int[] y)
public void pointerHoverPressed(int[] x, int[] y)
public void pointerHoverReleased(int[] x, int[] y)
public void longPointerPress(int x, int y)
public void keyPressed(int keyCode)
public void keyReleased(int keyCode)
public void keyRepeated(int keyCode)

Notice that most pointer events have a version that accepts an array as an argument, this allows for multi-touch event handling by sending all the touched coordinates. Desktop and pen-enabled devices can also trigger hover events without a press. To respond to those you can override the pointerHover* callbacks on Form or Component, which are invoked before a button receives focus or a drag begins on those platforms.

While you can override longPointerPress, there is no need. The dedicated Component.addLongPressListener() helper wires long press detection into an action listener so you can keep gesture logic in the high-level API.

Drag lifecycles also expose a completion hook. When Component.addDragFinishedListener() is registered it receives ActionEvent.Type.DragFinished once the framework has completed its cleanup, allowing you to reset state or trigger follow-up actions that should occur after the drag image is hidden.

5.2.2. Drag event sanitation

Drag events are quite difficult to handle across devices. Some devices send a ridiculous number of events for even the lightest touch while others send too little. It seems like too many drag events wouldn’t be a problem, but if you drag over a button then it might disable the buttons action event (since this might be the user trying to scroll).

Drag sensitivity is about the component being dragged which is why you have the method getDragRegionStatus that allows you to "hint" to the drag API whether you’re interested in drag events or not and if so in which directional bias.

For example, if your component is a painting app where you’re trying to draw using drag gestures you would use code such as:

public class MyComponent extends Component {
    protected int getDragRegionStatus(int x, int y) {
        return DRAG_REGION_LIKELY_DRAG_XY;
    }
}

This indicates that you want all drag events on both AXIS to be sent as soon as possible. Notice that this doesn’t disable event sanitation.

5.3. BrowserNavigationCallback

The BrowserNavigationCallback isn’t quite an "event" but there is no real "proper" classification for it.

The callback method of this interface is invoked off the EDT! You must NEVER block this method and must not access UI or Codename One sensitive elements in this method!

The browser navigation callback is invoked directly from the native web component as it navigates to a new page. Because of that it’s invoked on the native OS thread and gives you a unique opportunity to handle the navigation ourselves as you see fit. That’s why it MUST be invoked on the native thread, since the native browser is pending on your response to that method, spanning an invokeAndBlock/callSerially would be to slow and would bog down the browser.

6. Device Input and Form Factors

Codename One runs on a wide range of hardware, from phones and tablets to desktops, foldables, and devices driven by a mouse or a stylus. This chapter covers the cross-platform APIs that expose that hardware: rich pointer detail (mouse buttons, pen pressure and tilt, the pointing device type), the mouse wheel, foldable device posture, and multi-display / desktop-window awareness.

These APIs are additive: on a device or platform that doesn’t report a given capability the value falls back to a sensible default, so a single code base keeps working everywhere.

6.1. Rich pointer events

The classic pointerPressed, pointerReleased and pointerDragged callbacks carry only the x/y coordinates of the touch. Modern devices provide far more detail: which mouse button was used, the kind of pointing device (finger, mouse, stylus or eraser), the pressure applied, the stylus tilt, the contact area and the keyboard modifiers held at the time.

That detail is exposed through the immutable PointerEvent snapshot. There are two ways to read it.

6.1.1. From an ActionEvent

Any pointer listener receives an ActionEvent; call getPointerEvent() on it:

myComponent.addPointerPressedListener(e -> {
    PointerEvent pe = e.getPointerEvent();
    if (pe.isSecondaryButton()) {
        showContextMenu(pe.getX(), pe.getY());
    }
    System.out.println("pressure=" + pe.getPressure() + " type=" + pe.getPointerType());
});

6.1.2. By polling

When you’re inside a pointer callback you can also poll the current values directly from CN (or Display), mirroring the existing CN.isShiftKeyDown() family:

int button = CN.getPointerButton();      // PointerEvent.BUTTON_PRIMARY, BUTTON_SECONDARY, ...
float pressure = CN.getPointerPressure(); // 0.0 - 1.0, defaults to 1.0
boolean pen = CN.isStylusPointer();
PointerEvent current = CN.getCurrentPointerEvent();
Values have safe defaults. On a plain touch screen the button is BUTTON_PRIMARY, the pressure is 1.0 and the type is TYPE_TOUCH (or TYPE_UNKNOWN on ports that don’t classify the device).

6.2. Two and three button mice

PointerEvent.getButton() returns one of BUTTON_PRIMARY, BUTTON_SECONDARY, BUTTON_MIDDLE, BUTTON_BACK or BUTTON_FORWARD. CN.getPressedButtonMask() returns a bitmask of every button currently held (MASK_PRIMARY, MASK_SECONDARY, …​), which is useful during a drag.

The real button is reported on every desktop port - macOS, Windows and Linux - and in the browser, so the right, middle and (where the mouse has them) back and forward buttons are all distinguished. On a plain touch screen there is only the primary button.

For the common case of a context menu, use the unified addContextMenuListener. It fires for a secondary (right) mouse click, a stylus barrel button click and a long press, so the same code handles desktop and touch:

label.addContextMenuListener(e -> {
    e.consume(); // suppress the normal click/long-press handling
    showMyContextMenu(e.getX(), e.getY());
});

6.3. Mouse wheel and trackpad scrolling

By default the mouse wheel and trackpad scroll a scrollable container automatically. To react to the wheel yourself - for example control plus wheel to zoom, or horizontal scrolling - add a addMouseWheelListener. The listener receives a WheelEvent; consuming it prevents the default scroll.

A positive getDeltaY() reveals content above (the user scrolled down); a positive getDeltaX() reveals content to the left. isPrecise() is true for high resolution devices such as trackpads. On the desktop simulator, hold shift while turning the wheel to produce horizontal scrolling.

WheelEvent is a single universal scroll-gesture API: it’s fed by the mouse wheel and trackpad on the desktop, by the wheel / external trackpad on Android, and by the trackpad, Magic Mouse and mouse wheel on iOS and iPadOS (captured natively and routed through the same pipeline). Continuous trackpad scrolling reports isPrecise() == true, while a notched wheel reports false. On the Apple Watch the Digital Crown also feeds this same pipeline, so a list scrolls under the crown and a mouse wheel listener receives the crown as a WheelEvent.

6.4. Trackpad gestures (magnify and rotate)

Beyond scrolling, the trackpad reports magnify (pinch) and rotate gestures. Override pinch(float scale) to zoom and rotation(float radians) to rotate; both return true to mark the gesture as handled. pinch is also driven by a two finger pinch on touch screens, so the same code works on mobile and the desktop.

The trackpad gestures are wired on every desktop port: the macOS trackpad, the Windows touch screen and gesture-capable touchpads (through the system WM_GESTURE messages), and the Linux touchpad (through the GTK touchpad gesture events). scale is an incremental zoom factor (just above or below 1.0) and radians is the incremental rotation since the previous callback, so accumulate them to track the total.

A Windows precision touchpad usually reports a pinch as control plus wheel rather than a system gesture, so it also arrives through the universal wheel API. Handle that case with a addMouseWheelListener that checks isControlDown() (shown above) if you target those devices.

6.5. Stylus and pen support

When a stylus is used (Apple Pencil, S-Pen, Surface Pen) the pointer type is TYPE_STYLUS, or TYPE_ERASER for the eraser end. The PointerEvent then also carries:

  • getPressure() - normalized pressure between 0.0 and 1.0.

  • getTiltX() / getTiltY() - tilt in degrees (altitude and azimuth on iOS).

  • getContactSize() - normalized size of the contact area.

For drawing surfaces a dedicated addStylusListener fires on press, drag and release only when the pointer is a stylus or eraser:

drawingArea.addStylusListener(e -> {
    PointerEvent pe = e.getPointerEvent();
    float width = 1f + pe.getPressure() * 9f; // pressure controls stroke width
    if (pe.isEraser()) {
        erase(pe.getX(), pe.getY());
    } else {
        drawPoint(pe.getX(), pe.getY(), width);
    }
});

Pen hover (the pen moving above the screen without touching, available on devices such as the S-Pen and the M2 iPad with Apple Pencil) flows through the existing pointerHover callbacks; the snapshot reports isHovering() as true.

Touch-enabled desktops are handled too: on a Windows or Linux PC with a touch screen the finger reports TYPE_TOUCH, and a Windows pen reports TYPE_STYLUS, so the same drawing and gesture code runs on a touch laptop as on a phone. Display.getInstance().isTouchScreenDevice() is true on those machines.

6.6. Foldable devices and posture

Foldable and dual screen devices (Galaxy Fold, Galaxy Flip, Pixel Fold, Surface Duo) change shape at runtime. Query the live posture through DevicePosture:

DevicePosture p = CN.getDevicePosture();
// isTableTop() is true for book posture too, so check the hinge is
// horizontal before laying out top-and-bottom
if (p.isFoldable() && p.isTableTop()
        && p.getFoldOrientation() == DevicePosture.FOLD_ORIENTATION_HORIZONTAL) {
    layoutForTableTop(p.getFoldBounds(null));
}
  • getPosture() - POSTURE_FLAT, POSTURE_HALF_OPENED, POSTURE_CLOSED or POSTURE_UNKNOWN.

  • getHingeAngle() - hinge angle in degrees (-1 when unknown).

  • getFoldOrientation() - FOLD_ORIENTATION_VERTICAL, FOLD_ORIENTATION_HORIZONTAL or FOLD_ORIENTATION_NONE.

  • getFoldBounds(Rectangle) - the region the hinge occludes, in display coordinates, or null when there is no separating fold. Treat it like Form.getSafeArea(): keep interactive content out of it, or split a master and detail layout across the two halves.

React to fold changes with a posture listener:

class FoldAwareForm extends Form {
    private ActionListener postureListener;

    protected void initComponent() {
        super.initComponent();
        postureListener = e -> relayoutForPosture(CN.getDevicePosture());
        CN.addPostureListener(postureListener);
    }

    protected void deinitialize() {
        // CN registers on the singleton Display, so a screen that never
        // unregisters stays reachable and keeps receiving posture changes
        CN.removePostureListener(postureListener);
        super.deinitialize();
    }
}

To exercise this in the desktop simulator, use the Simulate → Foldable menu: enable foldable mode, then pick a posture (flat, half-opened or closed), the fold orientation (vertical or horizontal) and the hinge angle. The running app receives the same posture changes and listener callbacks it would on a real foldable device.

Foldable support on Android is opt-in so it never adds weight to apps that don’t use it. Enable it with the android.foldableSupport=true build hint (the version can be overridden with android.windowVersion). Note that the hinge angle isn’t available on Android, so getHingeAngle() returns -1 there.

6.7. Desktop windowing and external displays

Some mobile devices run in a desktop windowing mode where the app shares the screen with other windows: Samsung DeX, Android desktop windowing and iPad Stage Manager. CN.isDesktopMode() reports this. It’s distinct from CN.isDesktop(), which reports a genuine desktop platform (Windows, macOS or Linux).

CN.getDisplayCount() returns the number of attached displays and CN.isExternalDisplayConnected() is a convenience for getDisplayCount() > 1. On the desktop port these reflect the real monitors attached to the machine.

6.8. Apple TV remote

On Apple TV the Siri Remote is delivered through the standard key handling, so the same keyPressed/keyReleased and game-key code you write for other platforms works on the TV. The directional buttons map to the arrow / game keys, the click maps to enter (fire), the menu button maps to the back key, and play/pause maps to the media play/pause key. No TV specific API is required.

User interface

7. Basics: Themes, styles, components, and layouts

Start with a brief overview of the core ideas in Codename One. This chapter revisits them in more detail as it goes on.

7.1. Components

Every button, label, or other element on the screen in a Codename One application is a Component. This is a simplified version of the class hierarchy:

The Core Component Class Hierarchy
Figure 16. The Core Component Class Hierarchy

The Form is a special Component. It’s the root component shown to the user. Container is a Component type that can hold other components. That lets you create elaborate hierarchies by nesting Container instances.

Component UML
Figure 17. Component UML

A Codename One application is effectively a series of forms, but one Form can be visible at a time. The form contains everything on the screen. Under the hood, the Form has a few separate parts:

Structure of a Form
Figure 18. Structure of a Form
  • Content Pane - this is the body of the Form. When you add a Component to the Form, it goes into the content pane. The content pane scrolls vertically by default.

  • Title Area - you can’t add directly to this area. The title area is managed by the Toolbar class. Toolbar sits at the top of the form and handles the title design. The title area has two parts:

    • Title of the Form and its commands (the buttons on the right and left of the title)

    • Status Bar - on iOS, the top area leaves room for the notch, battery, clock, and similar indicators. Without that space, those elements would overlap the title.

Now that the structure is clear, open the Java file TodoApp.java in the project you created. You should see the lines that set up the UI in the start() method:

7.1.1. Layout managers

A layout manager decides the size and location of components within a Container. Every Container has a layout manager. The default layout manager is FlowLayout.

What each Codename One layout manager is for
Figure 19. What each layout manager is for

To understand layouts, first understand a basic concept about Component: each component has a “preferred size.” That’s the size a component wants to occupy. For example, a Label uses the exact size needed to fit the label text, icon, and padding.

Understanding Preferred Size

The Component class contains many useful methods. One of the most important is calcPreferredSize(), which recalculates the size a component wants when something changes.

By default Codename One invokes the getPreferredSize() method rather than calcPreferredSize() directly.
getPreferredSize() invokes calcPreferredSize() and caches the value.

The preferred size depends on internal constraints such as font size, border sizes, and padding.

When a layout manager positions and sizes the component, it MIGHT take the preferred size into account. It can also ignore it entirely.

For example, FlowLayout gives components their preferred size wherever the row has space for it, narrowing one that’s too wide and expanding them all when fillRows is set, while BorderLayout resizes the center component by default and the other components along one axis.

You can define a group of components to have the same preferred width or height by using the setSameWidth and setSameHeight methods, for example:

Listing 3. setSameWidth/Height
Component.setSameWidth(cmp1, cmp2, cmp3, cmp4);
Component.setSameHeight(cmp5, cmp6, cmp7);

Codename One has a setPreferredSize method that lets developers explicitly request a component size. But, that caused many problems. For example, preferred size should change with device orientation or similar operations. The API also encouraged accidental hardcoding of UI values such as forcing pixel sizes. As a result, the method was deprecated.

Use setSameWidth and setSameHeight when you need sizing alignment. Use setHidden when you need to hide a component. As a last resort, override calcPreferredSize.

A layout manager places a component based on its own logic and the preferred size, sometimes called the “natural size.” A FlowLayout walks the components in the order they were added and sizes them one after another. When it reaches the end of the row, it moves to the next row.

Use FlowLayout for simple things
FlowLayout is great for simple cases, but it has issues when components change size dynamically, such as a text field. In those cases, it can choose bad line breaks and take up too much space.
Layout Manager Primer Part I
Figure 20. Layout Manager Primer Part I
Layout Manager Primer Part II
Figure 21. Layout Manager Primer Part II

Scrolling doesn’t work well for all layout types because the positioning algorithm within a layout can break. Scrolling on the Y axis works well for BoxLayout Y, which is why it was picked for the TodoForm:

Table 2. Scrolling in Layout Managers
LayoutScrollable

Flow Layout

Possible on Y axis only

Border Layout

Scrolling is blocked

Box Layout Y

Scrollable only on the Y axis

Box Layout X

Scrollable only on the X axis

Grid Layout

Scrollable

LayeredLayout

Not scrollable (usually)

Nesting Scrollable Containers

Only one element can be scrollable within the hierarchy. Otherwise, if you drag your finger over the Form, Codename One can’t know which element you want to scroll. By default, the form’s content pane scrolls on the Y axis unless you disable it explicitly. Setting the layout to BorderLayout disables scrolling implicitly.

It’s fine to use non-scrollable layouts such as BorderLayout inside a scrollable container. In the TodoApp, for example, TodoItem uses BorderLayout inside a scrollable BoxLayout Form.

Layouts can be divided into two distinct groups:

  • Constraint Based - BorderLayout (and a few others such as GridBagLayout, MigLayout, and TableLayout)

  • Regular - All the other layout managers

When you add a Component to a Container with a regular layout you do so with a simple add method:

Listing 4. Adding to a Regular Container
Container cnt = new Container(BoxLayout.y());
cnt.add(new Label("Just Added"));

This works great for regular layouts but might not for constraint based layouts. A constraint based layout accepts another argument. For example: BorderLayout needs a location for the Component:

Listing 5. Adding to a Regular Container
cnt.add(NORTH, new Label("Just Added"));

This line assumes you have an import static com.codename1.ui.CN.*; in the top of the file. In BorderLayout (which is a constraint based layout) placing an item in the NORTH places it in the top of the Container.

The CN class is a class that contains many static helper methods and functions. It’s specifically designed for static import in this way to help keep your code terse
Static Global Context

The CN class is a thin wrapper around features in Display, NetworkManager, Storage, FileSystemStorage etc. It also adds common methods and constants from several other classes so Codename One code feels more terse for example, you can do:

That’s optional, if you don’t like static imports you can just write CN. for every element

From that point on you can write code that looks like this:

callSerially(() -> runThisOnTheEDT());

Instead of:

Display.getInstance().callSerially(() -> runThisOnTheEDT());

The same applies for most network manager calls e.g.:

addToQueue(myConnectionRequest);

Instead of:

NetworkManager.getInstance().addToQueue(myConnectionRequest);

Some things were changed so you won’t have too many conflicts for example, Log.p or Log.e would have been problematic so you now have:

log("my log message");
log(myException);

Instead of Display.getInstance().getCurrent() you now have getCurrentForm() since getCurrent() is too generic. For most methods you should just be able to remove the NetworkManager or Display access and it should "just work."

The motivation for this is threefold:

  • Terse code

  • Small performance gain

  • Cleaner API without some baggage in Display or NetworkManager

Some of your samples in this guide might rely on that static import being in place. This helps you keep the code terse and readable in the code listings.

Terse syntax

Almost every layout allows you to add a component using many variants of the add method:

Listing 6. Versions of add
Container cnt = new Container(BoxLayout.y());
cnt.add(new Label("Just Added")); (1)
cnt.addAll(new Label("Adding Multiple"), (2)
    new Label("Second One"));

cnt.add(new Label("Chaining")). (3)
    add(new Label("Value"));
1Regular add
2addAll accepts many components and adds them in a batch
3add returns the parent Container instance so you can chain calls like that

In the race to make code “tighter” you can make this even shorter. Most layout managers have their own custom terse syntax style for example:

Listing 7. Terse Syntax
Container boxY = BoxLayout.encloseY(cmp1, cmp2); (1)
Container boxX = BoxLayout.encloseX(cmp3, cmp4);
Container flowCenter = FlowLayout. (2)
    encloseCenter(cmp5, cmp6);
1Most layouts have a version of enclose to encapsulate components within
2FlowLayout has variants that support aligning the components on various axes

To sum this up, you can use layout managers and nesting to create elaborate UI’s that implicitly adapt to different screen sizes and device orientation.

Flow Layout
Flow Layout
Figure 22. Flow Layout

Flow layout lets the components "flow" horizontally and break a line when reaching the edge of the container. It’s the default layout manager for containers. Because it’s so flexible it’s also problematic as it can result in wrong preferred size values for the parent Container. This can create a reflow issue, as a result recommend using flow layout for trivial cases. Avoid it for things such as text input etc. As the size of the text input can vary in runtime:

Form hi = new Form("Flow Layout", new FlowLayout());
hi.add(new Label("First")).
    add(new Label("Second")).
    add(new Label("Third")).
    add(new Label("Fourth")).
    add(new Label("Fifth"));
hi.show();

Flow layout also supports terse syntax shorthand such as:

Container flowLayout = FlowLayout.encloseIn(
        new Label("First"),
        new Label("Second"),
        new Label("Third"),
        new Label("Fourth"),
        new Label("Fifth"));

Flow layout can be aligned to the left (the default), to the center, or to the right. It can also be vertically aligned to the top (the default), middle (center), or bottom.

Flow layout aligned to the center
Figure 23. Flow layout aligned to the center
Flow layout aligned to the right
Figure 24. Flow layout aligned to the right
Flow layout aligned to the center horizontally & the middle vertically
Figure 25. Flow layout aligned to the center horizontally & the middle vertically

Components within the flow layout get their natural preferred size by default and aren’t stretched in any axis.

The natural sizing behavior is often used to prevent other layout managers from stretching components. For example: if you have a border layout element in the south and you want it to keep its natural size instead of adding the element to the south directly you can wrap it using parent.add(BorderLayout.SOUTH, FlowLayout.encloseCenter(dontGrowThisComponent)).
Box Layout

BoxLayout places elements in a row (X_AXIS) or column (Y_AXIS) according to box orientation. Box is a simple and predictable layout that serves as the "workhorse" of component lists in Codename One.

You can create a box layout Y using something like this:

Form hi = new Form("Box Y Layout", new BoxLayout(BoxLayout.Y_AXIS));
hi.add(new Label("First")).
    add(new Label("Second")).
    add(new Label("Third")).
    add(new Label("Fourth")).
    add(new Label("Fifth"));

Which results in this

BoxLayout Y
Figure 26. BoxLayout Y

Box layout also supports a shorter terse notation which you use here to show the X axis box:

Container box = BoxLayout.encloseX(new Label("First"),
        new Label("Second"),
        new Label("Third"),
        new Label("Fourth"),
        new Label("Fifth"));
BoxLayout X
Figure 27. BoxLayout X

The box layout keeps the preferred size of its destination orientation and scales elements on the other axis. Specifically X_AXIS will keep the preferred width of the component while growing all the components vertically to match in size. Its Y_AXIS counterpart keeps the preferred height while growing the components horizontally.

This behavior is useful since it allows elements to align as they would all have the same size.

Sometimes the growing behavior in the X axis is undesired, for these cases you can use the X_AXIS_NO_GROW variant.

BoxLayout X_AXIS_NO_GROW
Figure 28. BoxLayout X_AXIS_NO_GROW
FlowLayout vs. BoxLayout.X_AXIS
When applicable recommend BoxLayout over FlowLayout as it acts more consistently in all situations. Another advantage of BoxLayout is the fact that it grows and thus aligns the components in a consistent column or row
Border Layout
Border Layout
Figure 29. Border Layout

Border layout is unique. BorderLayout is a constraint-based layout that can place up to five components in one of the five positions: NORTH, SOUTH, EAST, WEST or CENTER:

Form hi = new Form("Border Layout", new BorderLayout());
hi.add(BorderLayout.CENTER, new Label("Center")).
    add(BorderLayout.SOUTH, new Label("South")).
    add(BorderLayout.NORTH, new Label("North")).
    add(BorderLayout.EAST, new Label("East")).
    add(BorderLayout.WEST, new Label("West"));
hi.show();
The Constraints are Included in the CN class
You can use the static import of the CN class and then the syntax can be add(SOUTH, new Label("South"))

The layout always stretches the NORTH/SOUTH components on the X-axis to fill the container and the EAST/WEST components on the Y-axis. The center component is stretched to fill the remaining area by default. For example, the setCenterBehavior allows you to manipulate the behavior of the center component so it’s placed in the center without stretching.

For example:

Form hi = new Form("Border Layout", new BorderLayout());
((BorderLayout)hi.getLayout()).setCenterBehavior(BorderLayout.CENTER_BEHAVIOR_CENTER);
hi.add(BorderLayout.CENTER, new Label("Center")).
    add(BorderLayout.SOUTH, new Label("South")).
    add(BorderLayout.NORTH, new Label("North")).
    add(BorderLayout.EAST, new Label("East")).
    add(BorderLayout.WEST, new Label("West"));
hi.show();

Results in:

Border Layout with CENTER_BEHAVIOR_CENTER
Figure 30. Border Layout with CENTER_BEHAVIOR_CENTER
Scrolling is Disabled in Border Layout
Because of its scaling behavior scrolling a border layout makes no sense. Container implicitly blocks scrolling on a border layout, but it can scroll its parents/children

For RTL the EAST and WEST values are implicitly reversed as shown in this image:

Border Layout in RTL mode
Figure 31. Border Layout in RTL mode
RTL and Bidi

RTL (Right To Left) or Bidi (bi-directional) are common terms used for languages such as Hebrew, Arabic etc. These languages are written from the right to left direction hence all the UI needs to be “reversed.” Bidi denotes the fact that while the language is written from right to left, the numbers are still written in the other direction hence two directions…​

Preferred Size Still Matters
The preferred size of the center component doesn’t matter in border layout but the preferred size of the sides is. For example: If you place a large component in the SOUTH it will take up the entire screen and won’t leave room for anything
Grid Layout

GridLayout accepts a predefined grid (rows/columns) and grants all components within it equal size based on the dimensions of the largest components.

The main use case for this layout is a grid of icons for example: like one would see in the iPhone home screen

If the number of rows * columns is smaller than the number of components added a new row is implicitly added to the grid. For example, if the number of components is smaller than available cells (won’t fill the last row) blank spaces will be left in place.

In this example you can see that a 2×2 grid is used to add 5 elements, this results in an additional row that’s implicitly added turning the grid to a 3×2 grid implicitly and leaving one blank cell:

Form hi = new Form("Grid Layout 2×2", new GridLayout(2, 2));
hi.add(new Label("First")).
    add(new Label("Second")).
    add(new Label("Third")).
    add(new Label("Fourth")).
    add(new Label("Fifth"));
Grid Layout 2×2
Figure 32. Grid Layout 2×2

When you use a 2×4 size ratio you would see elements getting cropped as you do here. The grid layout uses the grid size first and doesn’t pay too much attention to the preferred size of the components it holds.

Grid Layout 2×4
Figure 33. Grid Layout 2×4

Grid also has an autoFit attribute that can be used to automatically calculate the column count based on available space and preferred width. This is useful for working with UI’s where the device orientation might change.

A terse syntax for working with a grid also exists in two versions, one that uses the "auto-fit" option and another that accepts the number of columns. Here’s a sample of the terse syntax coupled with auto-fit followed by screenshots of the same code in two orientations:

GridLayout.encloseIn(new Label("First"),
    new Label("Second"),
    new Label("Third"),
    new Label("Fourth"),
    new Label("Fifth"));
Grid Layout autofit portrait
Figure 34. Grid Layout autofit portrait
Grid Layout autofit landscape
Figure 35. Grid Layout autofit landscape

7.1.2. Table Layout

The TableLayout is an elaborate constraint based layout manager that can arrange elements in rows/columns while defining constraints to control complex behavior such as spanning, alignment/weight etc.

Note the Different Package for TableLayout
The TableLayout is in the com.codename1.ui.table package and not in the layouts package.
This is because TableLayout was originally designed for the Table class.

Despite being constraint based the TableLayout isn’t strict about constraints and will implicitly add a constraint when one is missing. This is unlike the BorderLayout which will throw an exception in this case.

Unlike GridLayout TableLayout won’t implicitly add a row if the row/column count is wrong
Form hi = new Form("TableLayout", new TableLayout(2, 2));
hi.add(new Label("First"));
hi.add(new Label("Second"));
hi.add(new Label("Third"));
hi.add(new Label("Fourth"));
hi.add(new Label("Fifth"));
hi.show();
2×2 TableLayout with 5 elements, notice that the last element is missing
Figure 36. 2×2 TableLayout with 5 elements, notice that the last element is missing

TableLayout supports the ability to grow the last column which can be enabled using the setGrowHorizontally method. You can also use a shortened terse syntax to construct a TableLayout but since the TableLayout is a constraint based layout you won’t be able to use its full power with this syntax.

The default usage of the encloseIn method below uses the setGrowHorizontally flag:

Container table = TableLayout.encloseIn(2,
        new Label("First"),
        new Label("Second"),
        new Label("Third"),
        new Label("Fourth"),
        new Label("Fifth"));
Form hi = new Form("TableLayout Enclose 2", new BorderLayout());
hi.add(BorderLayout.CENTER, table);
hi.show();
TableLayout.encloseIn() with default behavior of growing the last column
Figure 37. TableLayout.encloseIn() with default behavior of growing the last column
The full potential

TableLayout is a beast, to truly appreciate it you need to use the constraint syntax which allows you to span, align and set width/height for the rows and columns.

TableLayout works with a Constraint instance that can communicate your intentions into the layout manager. Such constraints can include more than one attribute for example: span and height.

TableLayout constraints can’t be reused for more than one component

The constraint class supports the following attributes

Table 3. Constraint Properties

column

The column for the table cell. This defaults to -1 which will place the component in the next available cell

row

Like column, defaults to -1 as well

width

The column width in percentages, -1 will use the preferred size. -2 for width will take up the rest of the available space

height

Like width but doesn’t support the -2 value

spanHorizontal

The cells that should be occupied horizontally defaults to 1 and can’t exceed the column count - current offset.

spanVertical

Like spanHorizontal with the same limitations

horizontalAlign

The horizontal alignment of the content within the cell, defaults to the special case -1 value to take up all the cell space can be either -1, Component.LEFT, Component.RIGHT or Component.CENTER

verticalAlign

Like horizontalAlign can be one of -1, Component.TOP, Component.BOTTOM or Component.CENTER

You need to set width/height to one cell in a column/row

The table layout constraint sample tries to show some unique things you can do with constraints.

The constraint sample below spans the title across all three columns and spans the notes row across two columns:

TableLayout layout = new TableLayout(4, 3);
layout.setGrowHorizontally(true);
Form hi = new Form("Table Layout", layout);

TableLayout.Constraint title = layout.createConstraint();
title.setHorizontalSpan(3);
title.setHorizontalAlign(Component.CENTER);
hi.add(title, new Label("Invoice"));

hi.add(new Label("Item"));
hi.add(new Label("Qty"));
hi.add(new Label("Total"));
hi.add(new Label("Design"));
hi.add(new Label("2"));
hi.add(new Label("$120"));

TableLayout.Constraint notes = layout.createConstraint();
notes.setHorizontalSpan(2);
notes.setHeightPercentage(40);
hi.add(notes, new SpanLabel("Notes span two columns"));
hi.add(new Button("Pay"));

hi.show();

That sample creates its constraints with a shorthand. Written out in full, a constraint is a separate object you configure and then pass in place of the usual layout argument:

TableLayout.Constraint cn = tl.createConstraint();
cn.setWidthPercentage(20);
hi.add(cn, new Label("AAA"));
TableLayout constraints can be used to create elaborate UI’s
Figure 38. TableLayout constraints can be used to create elaborate UI’s
TextMode Layout

TextModeLayout is a unique layout manager. It acts like TableLayout on Android and like BoxLayout.Y_AXIS in other platforms. Internally it delegates to one of these two layout managers so in a sense it doesn’t have as much functionality of its own.

For example: this is a sample usage of TextModeLayout:

TextModeLayout tl = new TextModeLayout(3, 2);
Form f = new Form("Pixel Perfect", tl);
TextComponent title = new TextComponent().label("Title");
TextComponent price = new TextComponent().label("Price");
TextComponent location = new TextComponent().label("Location");
TextComponent description = new TextComponent().label("Description").multiline(true);

f.add(tl.createConstraint().horizontalSpan(2), title);
f.add(tl.createConstraint().widthPercentage(30), price);
f.add(tl.createConstraint().widthPercentage(70), location);
f.add(tl.createConstraint().horizontalSpan(2), description);
f.setEditOnShow(title.getField());
f.show();
TextModeLayout on iOS
Figure 39. TextModeLayout on iOS
TextModeLayout on Android with the same code
Figure 40. TextModeLayout on Android with the same code

As you can see from the code and samples above there is a lot going on under the hood. On Android you want a layout that’s like TableLayout so you can “pack” the entries. On iOS you want a box layout Y kind of layout but you also want the labels/text to align…​

The TextModeLayout isn’t a layout as much as it’s a delegate. When running in the Android mode (which you refer to as the “on top” mode) the layout is almost an exact synonym of TableLayout and in fact delegates to an underlying TableLayout. In fact there is a public final table instance within the layout that you can refer to directly…​

One small difference exists between the TextModeLayout and the underlying TableLayout: your choice to default to align entries to TOP with this mode.

Aligning to TOP is important for error handling for TextComponent in Android otherwise the entries “jump”

When working in the non-android environment you use a BoxLayout on the Y axis as the delegate. One thing you do here differs from a default box layout: grouping. Grouping allows the labels to align by setting them to the same width, internally it invokes Component.setSameWidth(). Since text components hide the labels there is a special group method there that can be used. For example, this is implicit with the TextModeLayout which is pretty cool.

TextModeLayout was created specifically for the TextComponent and InputComponent so check out the section about them in the components chapter.

7.1.3. Layered Layout

When used without constraints, the LayeredLayout places the components in order one on top of the other and sizes them all to the size of the largest component. This is useful when trying to create an overlay on top of an existing component. For example: an "x" button to allow removing the component.

The X on this button was placed there using the layered layout code below
Figure 41. The X on this button was placed there using the layered layout code below

The code to generate this UI is slightly complex and contains few relevant pieces. The truly relevant piece is this block:

hi.add(LayeredLayout.encloseIn(settingsLabel,
        FlowLayout.encloseRight(close)));

You’re doing three distinct things here:

  1. You’re adding a layered layout to the form

  2. You’re creating a layered layout and placing two components within. This would be the equivalent of creating a LayeredLayout Container and invoking add twice

  3. You use FlowLayout to position the X close button in the right position

When used without constraints, the layered layout sizes all components to the exact same size one on top of the other. It requires that you use another container within; to position the components correctly

This is the full source of the example for completeness:

Form hi = new Form("Layered Layout");
int w = Math.min(Display.getInstance().getDisplayWidth(), Display.getInstance().getDisplayHeight());
Button settingsLabel = new Button("");
Style settingsStyle = settingsLabel.getAllStyles();
settingsStyle.setFgColor(0xff);
settingsStyle.setBorder(null);
settingsStyle.setBgColor(0xff00);
settingsStyle.setBgTransparency(255);
settingsStyle.setFont(settingsLabel.getUnselectedStyle().getFont().derive(w / 3, Font.STYLE_PLAIN));
FontImage.setMaterialIcon(settingsLabel, FontImage.MATERIAL_SETTINGS);
Button close = new Button("");
close.setUIID("Container");
close.getAllStyles().setFgColor(0xff0000);
FontImage.setMaterialIcon(close, FontImage.MATERIAL_CLOSE);
hi.add(LayeredLayout.encloseIn(settingsLabel,
        FlowLayout.encloseRight(close)));

Forms have a built in layered layout that you can access through getLayeredPane(), this allows you to overlay elements on top of the content pane.

The layered pane is used internally by components such as InteractionDialog, AutoComplete etc.

Codename One also includes a GlassPane that resides on top of the layered pane. Its useful if you want to "draw" on top of elements but is harder to use than layered pane
Insets and reference components

LayeredLayout supports insets for its children. This effectively allows you to position child components precisely where you want them, relative to their container or siblings. This functionality forms the under-pinnings of the GUI Builder’s Auto layout mode.

As an example, suppose you wanted to position a button in the lower right corner of its container. This can be achieved with LayeredLayout as follows:

Container cnt = new Container(new LayeredLayout());
Button btn = new Button("Submit");
LayeredLayout ll = (LayeredLayout)cnt.getLayout();
cnt.add(btn);
ll.setInsets(btn, "auto 0 0 auto");

The result is:

Button positioned in bottom right using insets

The thing new here is this line:

ll.setInsets(btn, "auto 0 0 auto");

This is called after btn has already been added to the container. It says that you want its insets to be "auto" on the top and left, and 0 on the right and bottom. This insets string follows the CSS notation of top right bottom left (that’s: start on top and go clockwise), and the values of each inset may be provided in pixels (px), millimetres (mm), percent (%), or the special "auto" value. Like CSS, you can also specify the insets using a 1, 2, or 3 values. For example:

  1. "1mm" - Sets 1mm insets on all sides.

  2. "1mm 2mm" - Sets 1mm insets on top and bottom; 2mm on left and right.

  3. "1mm 10% 2mm" - Sets 1mm on top, 10% on left and right, and 2mm on bottom.

  4. "1mm 2mm 1px 50%" - Sets 1mm on top, 2mm on right, 1px on bottom, and 50% on left.

auto insets

The special "auto" inset indicates that it’s a flexible inset. If all insets are set to "auto," then the component will be centered both horizontally and vertically inside its "bounding box."

The "inset bounding box" is the containing box from which a component’s insets are measured. If the component’s insets aren’t linked to any other components, then its inset bounding box will be the inner bounds (that’s: taking padding into account) of the component’s parent container.

If one inset is fixed (that’s: defined in px, mm, or %), and the opposite inset is "auto," then the "auto" inset will allow the component to be its preferred size. If you want to position a component to be centered vertically, and 5mm from the left edge, you could do:

ll.setInsets(btn, "auto auto auto 5mm");

Resulting in:

Button vertically centered 5mm from left edge
Figure 42. Button vertically centered 5mm from left edge

Move it to the right edge with:

ll.setInsets(btn, "auto 5mm auto auto");
% insets

Percent (%) insets are calculated about the inset bounding box. A 50% inset is measured as 50% of the length of the bounding box on the inset’s axis. For example: A 50% inset on top would be 50% of the height of the inset bounding box. A 50% inset on the right would be 50% of the width of the inset bounding box.

Insets, margin, and padding

A component’s position in a layered layout is determined as follows: (Assume that cmp is the component that you’re positioning, and cnt is the container (In pseudo-code):

x = cnt.paddingLeft + cmp.calculatedInsetLeft + cmp.marginLeft
y = cnt.paddingTop + cmp.calculatedInsetTop + cmp.marginTop
w = cnt.width - cnt.verticalScroll.width - cnt.paddingRight - cmp.calculatedInsetRight - cmp.marginRight - x
h = cnt.height - cnt.horizontalScroll.height - cnt.paddingBottom - cmp.calculatedInsetBottom - cmp.marginBottom - y
The calculatedInsetXXX values here will be the same as the corresponding provided inset if the inset has no reference component. If it does have a reference component, then the calculated inset will depend on the position of the reference component.

If no inset is specified, then it’s assumed to be 0. This ensures compatibility with designs that were created before layered layout supported insets.

Component references: Linking components together

If all you need to do is position a component relative to its parent container’s bounds, then mere insets provide you with enough vocabulary to achieve this. But most UIs are more complex than this and require another concept: reference components. Often you will want to position a component relative to another child of the same container. This is also supported.

For example, suppose you want to place a text field in the center of the form (both horizontally and vertically), and have a button placed beside it to the right. Positioning the text field is trivial (setInset(textField, "auto")), but there is no inset that you can provide that would position the button to the right of the text field. To do your goal, you need to set the text field as a reference component of the button’s left inset, which "links" the button’s left inset to the text field. Here is the syntax:

Container cnt = new Container(new LayeredLayout());
LayeredLayout ll = (LayeredLayout)cnt.getLayout();
Button btn = new Button("Submit");
TextField tf = new TextField();
cnt.add(tf).add(btn);
ll.setInsets(tf, "auto")
  .setInsets(btn, "auto auto auto 0")
  .setReferenceComponentLeft(btn, tf, 1f);

This would result in:

Button’s left inset linked to text field
Figure 43. Button’s left inset linked to text field

The two active lines here are the last two:

ll.setInsets(tf, "auto")
  .setInsets(btn, "auto auto auto 0")
  .setReferenceComponentLeft(btn, tf, 1f);
The reference position is defined as the distance, expressed as a fraction of the reference component’s length on the inset’s axis, between the reference component’s leading (outer) edge and the point from which the inset is measured. A reference position of 0 means that the inset is measured from the leading edge of the reference component. A value of 1.0 means that the inset is measured from the trailing edge of the reference component. A value of 0.5 means that the inset is measured from the center of the reference component. Etc. Any floating point value can be used, though the most common values are 0 and 1.

The definition above may make reference components and reference position seem more complex than they are. Some examples:

  1. For a top inset:

    1. referencePosition == 0 ⇒ the inset is measured from the top edge of the reference component.

    2. referencePosition == 1 ⇒ the inset is measured from the bottom edge of the reference component.

  2. For a bottom inset:

    1. referencePosition == 0 ⇒ the inset is measured from the bottom edge of the reference component.

    2. referencePosition == 1 ⇒ the inset is measured from the top edge of the reference component.

  3. For a left inset:

    1. referencePosition == 0 ⇒ the inset is measured from the left edge of the reference component.

    2. referencePosition == 1 ⇒ the inset is measured from the right edge of the reference component.

  4. For a right inset:

    1. referencePosition == 0 ⇒ the inset is measured from the right edge of the reference component.

    2. referencePosition == 1 ⇒ the inset is measured from the left edge of the reference component.

Layers In Codename One

Codename One allows placing components one on top of the other and you commonly use layered layout to do that. The form class has a built-in Container that resides in a layer on top of the content pane of the form.

When you add an element to a form it implicitly goes into the content pane. But, you can use getLayeredPane() and add any Component there. Such a Component will appear above the content pane. Notice that this layer resides below the title area (on the Y axis) and won’t draw on top of that.

When Codename One introduced the layered pane it was instantly useful. But, its popularity caused conflicts. Two separate pieces of code using the layered pane could collide with one another. Codename One solved it with getLayeredPane(Class c, boolean top). This method allocates a layer for a specific class within the layered pane. This way if two different classes use this method instead of the getLayeredPane() method they won’t collide. Each will get its own container in a layered layout within the layered pane seamlessly. The top flag indicates whether you want the layer to be the top most or bottom most layer within the layered pane (assuming it wasn’t created already). This allows you to place a layer that can appear above or below the already installed layers.

You only make use of the layered pane in this book but there are two more layers on top of it. The form layered pane is identical to the layered pane but spans the entire height of the Form (including the title area). As a result the form layered pane is slower as it needs to handle some special cases to support this functionality.

The glass pane is the top most layer, unlike the layered pane it’s purely a graphical layer. You can only draw on the glass pane with a Painter instance and a Graphics object. You can’t add components into that layer.

The Layered Pane
Figure 44. The Layered Pane

7.1.4. GridBag Layout

GridBagLayout was introduced to simplify the process of porting existing Swing/AWT code with a more familiar API. The API for this layout is problematic as it was designed for AWT/Swing where styles were unavailable. As a result it has its own insets API instead of using elements such as padding/margin.

Prefer TableLayout, which is just as capable and integrates better with Codename One.

The sample below is the Java tutorial’s GridBag example, ported to Codename One:

private static Button gridButton(String text) {
    Button button = new Button(text);
    button.setCapsText(false);
    Style style = button.getAllStyles();
    style.setBgColor(0xf4f8ff);
    style.setBgTransparency(255);
    style.setFgColor(0x0d47a1);
    style.setBorder(Border.createLineBorder(1, 0x2b5c9e));
    style.setPaddingUnit(Style.UNIT_TYPE_DIPS);
    style.setPadding(2, 2, 2, 2);
    return button;
}

public static Form createForm() {
    Form hi = new Form("GridBagLayout", new BorderLayout());
    Container grid = new Container(new GridBagLayout());
    Style gridStyle = grid.getAllStyles();
    gridStyle.setPaddingUnit(Style.UNIT_TYPE_DIPS);
    gridStyle.setPadding(4, 4, 4, 4);
    Button button;
    GridBagConstraints c = new GridBagConstraints();
    //natural height, maximum width
    c.fill = GridBagConstraints.HORIZONTAL;
    button = gridButton("One");
    c.weightx = 0.5;
    c.fill = GridBagConstraints.HORIZONTAL;
    c.gridx = 0;
    c.gridy = 0;
    grid.addComponent(c, button);

    button = gridButton("Two");
    c.fill = GridBagConstraints.HORIZONTAL;
    c.weightx = 0.5;
    c.gridx = 1;
    c.gridy = 0;
    grid.addComponent(c, button);

    button = gridButton("Three");
    c.fill = GridBagConstraints.HORIZONTAL;
    c.weightx = 0.5;
    c.gridx = 2;
    c.gridy = 0;
    grid.addComponent(c, button);

    button = gridButton("Long-Named Button 4");
    c.fill = GridBagConstraints.HORIZONTAL;
    c.ipady = 40;      //make this component tall
    c.weightx = 0.0;
    c.gridwidth = 3;
    c.gridx = 0;
    c.gridy = 1;
    grid.addComponent(c, button);

    button = gridButton("5");
    c.fill = GridBagConstraints.HORIZONTAL;
    c.ipady = 0;       //reset to default
    c.weighty = 1.0;   //request any extra vertical space
    c.anchor = GridBagConstraints.PAGE_END; //bottom of space
    c.insets = new Insets(10,0,0,0);  //top padding
    c.gridx = 1;       //aligned with button 2
    c.gridwidth = 2;   //2 columns wide
    c.gridy = 2;       //third row
    grid.addComponent(c, button);
    hi.add(BorderLayout.NORTH, grid);
    return hi;
}

Because of the way GridBag works there’s no terse syntax for it, although one should be possible.

GridBagLayout sample from the Java tutorial running on Codename One
Figure 45. GridBagLayout sample from the Java tutorial running on Codename One

7.1.5. Group Layout

GroupLayout is a layout that would be familiar to the users of the NetBeans GUI builder (Matisse). Its a layout manager that’s hard to use for manual coding but is powerful for some elaborate use cases. Although MigLayout and LayeredLayout might be superior options.

It was originally added during the LWUIT days as part of an internal try to port Matisse to LWUIT. It’s still useful to this day as developers copy and paste Matisse code into Codename One and produce elaborate layouts with drag.

Since the layout is based on an older version of GroupLayout some things need to be adapted in the code or you should use the special "compatibility" library for Matisse to get better interaction. You also recommend tweaking Matisse to use import statements instead of full package names, that way if you use Label changing the awt import to a Codename One import will make it use work for Codename One’s Label.

Unlike any other layout manager GroupLayout adds the components into the container instead of the standard API. This works for GUI builder code but as you can see from this sample it doesn’t make the code readable:

public static Form createForm() {
    Form hi = new Form("GroupLayout");

    Label label1 = new Label();
    Label label2 = new Label();
    Label label3 = new Label();
    Label label4 = new Label();
    Label label5 = new Label();
    Label label6 = new Label();
    Label label7 = new Label();

    label1.setText("label1");

    label2.setText("label2");

    label3.setText("label3");

    label4.setText("label4");

    label5.setText("label5");

    label6.setText("label6");

    label7.setText("label7");

    GroupLayout layout = new GroupLayout(hi.getContentPane());
    hi.setLayout(layout);
    layout.setHorizontalGroup(
        layout.createParallelGroup(GroupLayout.LEADING)
        .add(layout.createSequentialGroup()
            .addContainerGap()
            .add(layout.createParallelGroup(GroupLayout.LEADING)
                .add(layout.createSequentialGroup()
                    .add(label1, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
                    .addPreferredGap(LayoutStyle.RELATED)
                    .add(layout.createParallelGroup(GroupLayout.LEADING)
                        .add(label4, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
                        .add(label3, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
                        .add(label2, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)))
                .add(label5, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
                .add(layout.createSequentialGroup()
                    .add(label6, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
                    .addPreferredGap(LayoutStyle.RELATED)
                    .add(label7, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)))
            .addContainerGap(296, Short.MAX_VALUE))
    );
    layout.setVerticalGroup(
        layout.createParallelGroup(GroupLayout.LEADING)
        .add(layout.createSequentialGroup()
            .addContainerGap()
            .add(layout.createParallelGroup(GroupLayout.TRAILING)
                .add(label2, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
                .add(label1, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE))
            .addPreferredGap(LayoutStyle.RELATED)
            .add(label3, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
            .addPreferredGap(LayoutStyle.RELATED)
            .add(label4, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
            .addPreferredGap(LayoutStyle.RELATED)
            .add(label5, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
            .addPreferredGap(LayoutStyle.RELATED)
            .add(layout.createParallelGroup(GroupLayout.LEADING)
                .add(label6, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
                .add(label7, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE))
            .addContainerGap(150, Short.MAX_VALUE))
    );
    return hi;
}
GroupLayout Matisse generated UI running in Codename One
Figure 46. GroupLayout Matisse generated UI running in Codename One

If you’re porting newer Matisse code there are simple changes you can do:

  • Change addComponent to add

  • Change addGroup to add

  • Remove references to ComponentPlacement and reference LayoutStyle directly

7.1.6. Mig Layout

MigLayout is a popular cross-platform layout manager that was ported to Codename One from Swing.

MiG is still considered experimental so proceed with caution!
The API was deprecated to serve as a warning of its experimental status.

The best reference for MiG would probably be its quick start guide (PDF link). The sample below is one of that guide’s examples, ported to Codename One:

public static Form createForm() {
    Form hi = new Form("MigLayout",
            new MigLayout("wrap 2, insets 4mm", "[right]3mm[32mm]", "[]12[]12[]12[]"));
    hi.add(new Label("First name"));
    hi.add("growx, w 32mm", new TextField("", "First name"));
    hi.add(new Label("Last name"));
    hi.add("growx, w 32mm", new TextField("", "Last name"));
    hi.add(new Label("Phone"));
    hi.add("growx, w 32mm", new TextField("", "Phone"));
    Button ok = new Button("OK");
    ok.setCapsText(false);
    Style okStyle = ok.getAllStyles();
    okStyle.setBgColor(0xf4f8ff);
    okStyle.setBgTransparency(255);
    okStyle.setFgColor(0x0d47a1);
    okStyle.setBorder(Border.createLineBorder(1, 0x2b5c9e));
    okStyle.setPaddingUnit(Style.UNIT_TYPE_DIPS);
    okStyle.setPadding(2, 2, 2, 2);
    hi.add("span 2, growx", FlowLayout.encloseCenter(ok));
    return hi;
}
MiG layout sample ported to Codename One
Figure 47. MiG layout sample ported to Codename One

It should be reasonably easy to port MiG code but you should notice the following:

  • MiG handles a lot of the spacing/padding/margin issues that are missing in Swing/AWT. With Codename One styles you have the padding and margin which are probably a better way to do a lot of the things that MiG does

  • The add method in Codename One can be changed as shown in the sample above.

  • The constraint argument for Codename One add calls appears before the Component instance.

7.2. Themes and styles

Next you need to introduce you to 3 important terms in Codename One: Theme, Style, and UIID.

Themes are similar conceptually to CSS, in fact they can be created with CSS syntax as this guide covers soon. The various Codename One ports ship with a native theme representing the appearance of the native OS UI elements. Every Codename One application has its own theme that derives the native theme and overrides behavior within it.

If the native theme has a button defined, you can override properties of that button in your theme. This allows you to customize the look while retaining some native appearances. This works by merging the themes to one big theme where your application theme overrides the definitions of the native theme. This is pretty like the cascading aspect of CSS if you’re familiar with that.

Themes consist of a set of UIID definitions. Every component in Codename One has a UIID associated with it. UIID stands for User Interface Identifier. This UIID connects the theme to a specific component. A UIID maps to CSS classes if you’re familiar with that concept. For example, Codename One doesn’t support the complex CSS selector syntax options as those can impact runtime performance.

For example: see this code where:

Listing 8. setUIID on TextField
nameText.setUIID("Label");

This is a text field component (user input field) but it will look like a Label.

Effectively you told the text field that it should use the UIID of Label when it’s drawing itself. It’s common to do tricks like that in Codename One. For example: button.setUIID("Label") would make a button appear like a label and allow you to track clicks on a Label.

The UIID’s translate the theme elements into a set of Style objects. These Style objects get their initial values from the theme but can be further manipulated after the fact. To make the text field’s foreground color red you could use this code:

Listing 9. Setting a style directly
nameText.getAllStyles().setFgColor(0xff0000);

The color is in hexadecimal RRGGBB format so 0xff00 would be green and 0xff0000 would be red.

getAllStyles() returns a Style object but why do you need “all” styles?

Each component can have one of 4 states and each state has a Style object. This means you can have 4 style objects per Component:

  • Unselected: used when a component isn’t touched and doesn’t have focus. You can get that object with getUnselectedStyle().

  • Selected: used when a component is touched or if focus is drawn for non-touch devices. You can get that object with getSelectedStyle().

  • Pressed: used when a component is pressed. Notice it’s applicable to buttons and button subclasses. You can get that object with getPressedStyle().

  • Disabled: used when a component is disabled. You can get that object with getDisabledStyle().

The getAllStyles() method returns a special case Style object that lets you set the values of all 4 styles from one class so the code before would be equivalent to invoking all 4 setFgColor methods. For example, getAllStyles() works for setting properties not for getting them!

don’t use getStyle() for manipulation
getStyle() returns the current Style object which means it will behave inconsistently. The paint method uses getStyle() as it draws the current state of the Component but other code should avoid that method. Use the specific methods instead: getUnselectedStyle(), getSelectedStyle(), getPressedStyle(), getDisabledStyle() and getAllStyles()

As you can see, it’s a bit of a hassle to change styles from code which is why the theme is so appealing.

7.2.1. Theme

A theme allows you to define the styles externally through a set of UIID’s (User Interface ID’s). Themes can be authored directly in CSS and then compiled into the Codename One resource file, which keeps styling concerns separate from application logic.

The theme is stored in the theme.res file in the project.

You load the theme file using this line of code in the init(Object) method in the main class of the application:

Listing 10. Theme Loading Code
theme = UIManager.initFirstTheme("/theme");

In a CSS project this file is generated automatically from the stylesheet. Legacy applications that still edit the resource file by hand can continue to do so, but new projects should prefer the CSS workflow described below.

This code is shorthand for resource file loading and for the installation of theme. You could technically have more than one theme in a resource file at which point you could use initNamedTheme() instead. The resource file is a special file format that includes inside it many features:

  • Themes

  • Images

  • Localization Bundles

  • Data files

7.2.2. Working with CSS themes

Modern Codename One projects ship with a src/main/css/theme.css file (or an equivalent stylesheet). Editing this file allows you to define UIIDs using standard CSS syntax together with Codename One–specific extensions such as cn1-derive for inheritance and the #Constants block for theme constants. Each time you build or run the project, the build tool compiles the CSS into the theme.res resource file automatically. Saving the CSS while the simulator is running will also trigger a refresh so you can iterate on styling.

Because the CSS compiler produces the final resource file, you should treat the generated theme.res as an output artifact and keep your changes in the CSS source. Images referenced from CSS rules (for example: background images or multi-images) should be placed alongside the stylesheet so that they’re picked up by the compiler. More details about the supported selectors and properties are covered in the dedicated CSS chapter later in this guide.

7.3. GUI builder

The GUI builder arranges components visually: you drag them onto a canvas, position them, and edit their properties in an inspector, without writing the layout code by hand. It’s a desktop application that the Codename One Maven plugin launches, so it works the same way whichever IDE you use for the surrounding Java code. Forms designed in the builder use auto layout mode by default, which positions and resizes components on the canvas using LayeredLayout behind the scenes.

7.3.1. Hello world

Two goals do the work:

  • cn1:create-gui-form generates a new GUI form: the .gui XML file plus the matching Java source.

  • cn1:guibuilder opens the GUI builder on the project.

Both goals accept a className parameter that points at the fully qualified class name of the form. From the root of a multi-module Codename One project, create a new form like this:

mvn cn1:create-gui-form -DclassName=com.example.MyForm

This generates two files inside the common module:

  1. common/src/main/guibuilder/com/example/MyForm.gui

  2. common/src/main/java/com/example/MyForm.java

If you run the goal from inside the common submodule the files are written directly under that module’s src/main/…​ directory. By default the goal creates a Form; pass -DguiType=Dialog or -DguiType=Container to generate one of the other types, and pass -DautoLayout=false to opt out of auto layout mode.

To open the form in the GUI builder:

mvn cn1:guibuilder -DclassName=com.example.MyForm

Projects generated from the archetype also carry a ready-made shortcut for this: a CN1 GUI Builder run configuration in IntelliJ IDEA, an Open in GUI Builder action in NetBeans, a GUI Builder launch configuration in Eclipse, and a Tools > GUI Builder entry in the Visual Studio Code Maven favorites. They all run the same goal.

The full goal reference, including every parameter, lives in the cn1:create-gui-form goal and the cn1:guibuilder goal in the Maven goals appendix.

The GUI builder is a Java 8 artifact, so it runs on whichever JDK already builds your project.

7.3.2. The workspace

The GUI builder editing a newly created form
Figure 48. The GUI builder editing a form created with cn1:create-gui-form

The window has three columns:

  • Project forms and palette — on the left. The upper half lists every .gui file in the project under the Forms tab, and the component tree of the form you are editing under the Hierarchy tab. Picking a component from the tree is often easier than hitting it on the canvas, particularly when it’s behind something else. The lower half is the component palette, with a search field for finding a component by name.

  • Design canvas — in the middle. This is a live Codename One rendering of your form, styled by the project’s own theme.css, so what you see is what the application draws. The buttons above the canvas switch it between phone portrait, phone landscape, tablet, and a full-width desktop canvas.

  • Inspector — on the right. It shows the selected component under three tabs: Properties, Layout, and Events.

The toolbar carries Save, Undo, Redo and Refresh on the left, and CSS and Code on the right. Changes are written to the .gui file when you press Save.

The two dividers between the columns can be dragged, so you can give the canvas more room while positioning components and give the inspector more room while filling in properties.

7.3.3. Designing a form

Drag a component from the palette onto the canvas to add it. As you drag, the builder highlights where the component would land: the container that would receive it, and, in auto layout mode, guides showing the edges it would line up with. Dropping outside a valid target leaves the form unchanged.

Select a component by clicking it on the canvas or by picking it in the Hierarchy tab. A selected component shows resize handles; drag its body to move it, or a handle to resize it. Hold Shift while clicking to select several components at once, which is what the alignment actions in the top-left corner of the canvas operate on: they align edges, centers or baselines, match widths or heights, or disconnect a component from the relationships it has picked up.

Double-click a component that displays text — or long-press it — to edit that text in place, without going to the inspector.

The Properties tab holds what the component is: its name, its text, the UIID that connects it to a CSS selector, and the settings that belong to its type, such as the input constraint of a text field or the range of a slider. The Layout tab holds where it sits: its position in its parent, the layout manager of a container, and, for auto layout, its alignment reference and its horizontal and vertical size policies. The Events tab binds an event, such as a button press, to a method in the companion Java source.

7.3.4. Auto layout mode

New forms use auto layout mode. In this mode you place components where you want them rather than accepting the positions a layout manager dictates.

All forms designed in auto layout mode use LayeredLayout
Auto layout mode is built on the inset support in LayeredLayout. Component positioning uses insets and reference components, not absolute coordinates.

An inset can be fixed (stated in millimetres, pixels or a percentage) or flexible, and it can be measured from the parent or from a sibling component. That’s what makes a form designed on one canvas size still work on another: a component pinned to the bottom of the form stays there on a taller screen, and a component pinned below its neighbor follows that neighbor when it moves.

The builder chooses insets for you as you drag, and it prefers a relationship with a nearby component over a distance from the edge of the form. The Layout tab shows the choice it made and lets you change it:

  • Alignment reference — the component this one is positioned against, or the nearest component if you haven’t chosen one.

  • Horizontal size policy and Vertical size policy — whether the component keeps its preferred size, keeps a fixed size, fills its parent, or matches the size of its reference component.

Resize the canvas with the device buttons above it after every few changes. A form that looks right on one canvas can fall apart on another, and switching between phone portrait and desktop is the quickest way to find out before a device does.

7.3.5. Nested containers and other layouts

A form doesn’t have to be one flat surface. Drag a Container from the palette to group components together, then use the Container layout picker in the Layout tab to give it whichever layout manager suits that part of the form: box, border, flow, grid, table, or layered again. Components dragged into that container are then arranged by it, and the drop guides change to match — a box layout shows where in the sequence the component would go, a border layout shows which region would receive it, and a table layout shows the cell.

Containers can be nested as far as you need, and a component can be dragged out of one container and into another; the builder rewrites its constraints for the layout it lands in.

7.3.6. The theme and the companion source

The CSS button opens the project stylesheet inside the builder.

Editing theme.css inside the GUI builder
Figure 49. Editing the project stylesheet; the canvas re-renders as you type

The stylesheet is compiled and applied to the canvas as you edit it, so a color or a font change is visible immediately on the form you are designing. This is the same src/main/css/theme.css the application builds with, described in Working with CSS themes.

The Code button opens the companion Java source.

The generated companion source
Figure 50. The companion Java source, generated from the form

Everything between the // <gui-builder-generated> and // </gui-builder-generated> markers is written by the builder from the .gui file and is replaced whenever the form is saved. The region between the // <gui-builder-user-code> markers is yours: event handlers and anything else you add there is preserved across saves. The editor makes the distinction visible, and the status line reminds you which region takes your changes.

Forms scaffolded by older versions of cn1:create-gui-form used a different marker style. Opening such a form in the builder converts the file to the format above and keeps the methods you had written.

7.3.7. What’s stored on disk

A form is two files that belong together:

  • common/src/main/guibuilder/<package>/<Name>.gui — the XML description of the component tree. This is the file the builder reads and writes, and the one to keep under version control.

  • common/src/main/java/<package>/<Name>.java — the companion Java source.

The two files are connected by name. If you rename or move one, rename or move the other to match, or the builder can no longer pair them.

The .gui file is plain XML and readable enough to review in a diff:

<?xml version="1.0" encoding="UTF-8"?>

<component type="Form" name="SignInForm" title="Sign in" layout="LayeredLayout" autoLayout="true">
    <component type="Label" name="heading" text="Welcome back" layeredInsets="12% auto auto 8%" />
    <component type="TextField" name="email" hint="Email" layeredInsets="26% 8% auto 8%"
        guidedReferences="heading|-|-|-" />
    <component type="Button" name="signIn" text="Sign in" actionEvent="onSignIn"
        layeredInsets="62% 8% auto 8%" guidedReferences="email|-|-|-" />
</component>

You can edit it by hand, but reopen the form afterward: the builder doesn’t watch the file while it’s running.

8. The components of Codename One

This chapter covers the components of Codename One. It doesn’t cover every component, but it goes deeper than the Javadocs.

8.1. Container

The Codename One container is a base class for many high-level components. A container can hold other components.

Every component has a parent container. That parent can be null when the component isn’t inside another container or when it’s a top-level container. A container can have many children.

Component-Container relationship expressed as UML
Figure 51. Component-Container relationship expressed as UML

Components are arranged in containers with layout managers. A layout manager decides how to arrange components inside the container.

You can read more about layout managers in the basics section.

8.1.1. Composite components

Codename One components share a generic inheritance hierarchy. For example, Button derives from Label and thus receives all its abilities.

Some components are composites and derive from the Container class. For example, the MultiButton is a composite button that derives from Container but behaves like a Button. Most of the time this feels seamless, but a few details matter.

  • Don’t use the Container-derived methods on such a composite component (for example, add and remove).

  • You can’t cast it to the type it relates to. For example, you can’t cast MultiButton to Button.

  • Events can be more nuanced. For example, if you rely on ActionEvent.getSource() or ActionEvent.getComponent(), they may not behave as expected. For a MultiButton, they return the underlying Button. To work around that, use ActionEvent.getActualComponent().

Lead Component

Codename One includes a feature for creating composite components called "lead components." It lets components like MultiButton act like a single component while still containing multiple components.

Lead components work by assigning one component as the "leader." That leader determines the style state for every component in the hierarchy. If a Container is led by a Button, the button determines whether the selected or pressed state applies to the entire hierarchy.

This means a single Component can contain multiple nested UIID`s. For example, `MultiButton has UIID`s such as `MultiLine1, which you can customize with APIs such as setUIIDLine1.

The lead component also handles events from a single source. Clicking another component in the hierarchy sends the event to the leading Button, which can make action events route to a different target than the apparent click target. That’s why getActualComponent() exists.

You can learn more about lead components in here.

8.2. Form

A Form isn’t one surface. It’s a title area, a content pane, any number of layered panes, and on ports that place commands in a soft-key or button bar, a menu bar as well. Knowing which of them receives a component explains most of what happens next:

The parts of a Form: title area, content pane, layered pane and safe area
Figure 52. The parts of a Form, and which accessor reaches each one

Form is the top-level container in Codename One. Form derives from Container and is the element the app shows. Only one form can be visible at any given time. You can get the visible Form with the code:

Form currentForm = Display.getInstance().getCurrent();

A form is a container with a title, a content area, and an optional menu or menu-bar area. When you call methods such as add or remove on a form, you’re invoking something that maps to this:

myForm.getContentPane().add(cmp);

Form is effectively a Container with a border layout. The north section holds the title area, the south section holds the optional menu bar, and the center stretches as the content pane. Place all components in the content pane.

Form layout graphic
Figure 53. Form layout graphic

You can see that every Form has space allocated for the title area. If you don’t set the title, it won’t show up, but it will still be there. The same isn’t always true for the menu bar, which can vary. Effectively, the section that matters is the content pane, so the form tries to do the right thing by pretending to be the content pane. For example, this isn’t always seamless, and sometimes code needs to invoke getContentPane() to work directly with the container.

A good example for such a case is layout animations. Animating the form might not produce the right results. When in doubt, it’s easier to use getContentPane() instead of working with the Form directly.

As you can see from the graphic, Form has two layers that live on top of the content pane/title. The first is the layered pane, which allows you to place "always on top" components. The layered pane is added implicitly when you invoke getLayeredPane().

You still need to place components using layout managers to get them to appear in the right place when using the layered pane.

8.2.1. Safe areas

Modern phones often include display cut-outs, rounded corners, or persistent gesture/navigation indicators that overlap the physical screen edges. These intrusions are no longer limited to iOS—many recent Android devices behave the same way—so Codename One provides a "safe area" API to ensure your UI remains visible regardless of the device. The safe area is the portion of the display that’s guaranteed to be unobstructed.

Tabs with safe-area padding enabled
Figure 54. Tabs with safe-area padding enabled
Tabs without safe-area padding
Figure 55. Tabs without safe-area padding
Automatic Safe-Area handling

Form calculates the safe region automatically, and core UI components that anchor themselves to the screen edges apply that information without more work:

  • Toolbar and the status bar placeholder respect the top safe margins so that titles and action buttons remain legible around notches.

  • Tabs, Sheet, and FloatingActionButton instances mark their internal containers as safe areas so navigation controls are padded above gesture/navigation bars.

  • UI fragment templates can opt-in to safe areas declaratively using the safeArea flag, ensuring generated components stay within the padded region.

This means that many applications will work on devices such as the iPhone X/14 family or Android devices with edge-to-edge displays. You need to intervene when you use custom containers that you position flush against a screen edge or when you perform your own painting.

Marking your own containers as safe

If you create a container that should always stay clear of the unsafe portions of the screen (for example, a bottom navigation bar, an on-screen joystick, or a floating tool palette), enable safe-area padding explicitly:

Form form = new Form(new BorderLayout());

Container bottomBar = new Container(BoxLayout.x());
bottomBar.setSafeArea(true);
bottomBar.addAll(new Button("Home"), new Button("Search"), new Button("Profile"));

form.add(BorderLayout.SOUTH, bottomBar);
form.show();

Safe-area padding is applied when the container doesn’t have a scrollable parent. For scrollable content, you can assume the user can scroll the component into view instead.

Most layouts never need to know where the safe area begins, but if you draw manually (for example, inside paint() or on the glass pane) you can query it directly:

Form form = Display.getInstance().getCurrent();
Rectangle safe = form.getSafeArea();

Graphics g = null; // e.g. inside paint()
g.setClip(safe.getX(), safe.getY(), safe.getWidth(), safe.getHeight());
// Custom drawing code that should avoid the notch/gesture areas

The rectangle returned by Form#getSafeArea() is updated automatically whenever the OS reports a change (rotation, multitasking gestures, showing or hiding the system navigation area, etc.). In unusual situations where you adjust the safe-area root yourself (for example, when animating a container in from off-screen) you can force a recalculation by calling Form#setSafeAreaChanged().

Safe-Area roots and advanced layouts

Safe-area padding is calculated relative to a "safe-area root." Forms are roots by default, but you can mark any container as a root using Container#setSafeAreaRoot(true) when you need precise control, for example, when preparing a side menu that starts off-screen and slides in:

Container drawer = new Container(BoxLayout.y());
drawer.setSafeAreaRoot(true); // Ensure safe margins apply before the drawer is visible
drawer.setSafeArea(true);

Marking the drawer as both a root and a safe area prevents a jump the moment it becomes visible, because the safe padding is already applied while it’s off-screen.

Remember that safe areas apply across platforms. Always verify your screens on actual devices (or in the Codename One simulator with a device skin that exposes cut-outs) to make sure critical UI elements remain inside the padded region.

The second layer is the glass pane which allows you to draw arbitrary things on top of everything. The order in the image is indeed right:

  1. ContentPane is lowest

  2. LayeredPane is second

  3. GlassPane is painted last

It’s important to notice that a layered pane is on top of the ContentPane and doesn’t stretch to the title. A GlassPane stretches all the way but with a "lightweight" title area, for example, the Toolbar API.

The GlassPane allows developers to overlay UI on top of existing UI and paint as they see fit. This is useful for things that provide notification but don’t want to intrude with application functionality.

8.3. Dialog

A Dialog is a special kind of Form that can occupy a portion of the screen. It also has the more functionality of the modal show method.

When showing a dialog you have two basic options: modeless and modal:

  • Modal dialogs (the default) block the current EDT thread until the dialog is dismissed. To understand how they do it, read about invokeAndBlock.
    Modal dialogs are a useful way to prompt the user since the code can assume the user responded in the next line of execution. This promotes a linear and intuitive way of writing code.

  • Modeless dialogs return, so a call to show such a dialog can’t assume anything in the next line of execution. This is useful for features such as progress indicators where you aren’t waiting for user input.

For example: a modal dialog can be expressed as such:

if(Dialog.show("Click Yes Or No", "Select one", "Yes", "No")) {
    // user clicked yes
} else {
    // user clicked no
}

Notice that during the show call above the execution of the next line was "paused" until you got a response from the user and once the response was returned you could proceed directly.

All usage of Dialog must be within the Event Dispatch Thread (the default thread of Codename One). This is true for modal dialogs. The Dialog class knows how to "block the EDT" without blocking it.

To learn more about invokeAndBlock which is the workhorse behind the modal dialog functionality check out the EDT section.

The Dialog class contains many static helper methods to show user notifications, but also allows a developer to create a Dialog instance, add information to its content pane and show the dialog.

Dialogs contain a ContentPane like Form.

When showing a dialog in this way, you can either ask Codename One to position the dialog in a specific location based on the BorderLayout concept of locations, or position it by spacing it in pixels from the four edges of the screen.

For example: you could do something like this to show a simple modal Dialog:

Dialog d = new Dialog("Title");
d.setLayout(new BorderLayout());
d.add(BorderLayout.CENTER, new SpanLabel("Dialog Body", "DialogBody"));
d.showPacked(BorderLayout.SOUTH, true);
Custom Dialog in the south position
Figure 56. Custom modal Dialog in the south position
You can turn the code above to a modless Dialog by flipping the boolean true argument to false.

You can position a Dialog by determining the space from the edges for example: with this code you can occupy the bottom portion of the screen:

Dialog d = new Dialog("Title");
d.setLayout(new BorderLayout());
d.add(BorderLayout.CENTER, new SpanLabel("Dialog Body", "DialogBody"));
d.show(hi.getHeight() / 2, 0, 0, 0);
Custom Dialog positioned
Figure 57. Custom Dialog positioned absolutely
hi is the name of the parent Form in the sample above.

8.3.1. Styling dialogs

It’s important to style a Dialog using getDialogStyle() or setDialogUIID methods rather than styling the dialog object directly.

The reason for this is that the Dialog is a Form that takes up the whole screen. The Form that’s visible behind the Dialog is rendered as a screenshot. Customizing the actual UIID of the Dialog won’t produce the desired results.

8.3.2. Tint and blurring

By default a Dialog uses a platform specific tint color when it’s showing for example: notice the background in the image below is tinted:

Form hi = new Form("Tint Dialog", new BoxLayout(BoxLayout.Y_AXIS));
Button showDialog = new Button("Tint");
showDialog.addActionListener((e) -> Dialog.show("Tint", "Is On/* omitted */", "OK", null));
hi.add(showDialog);
hi.show();
Dialog with tinted background
Figure 58. Dialog with tinted background

The tint color can be manipulated on the parent form, you can set it to any AARRGGBB value to set any color using the setTintColor method. Notice that this is invoked on the parent form and not on the Dialog!

This is an AARRGGBB value and not an RRGGBB value! This means that 0 will be transparent.

You can also manipulate this default value globally using the theme constant tintColor. The sample below tints the background in green:

Form hi = new Form("Tint Dialog", new BoxLayout(BoxLayout.Y_AXIS));
hi.setTintColor(0x7700ff00);
Button showDialog = new Button("Tint");
showDialog.addActionListener((e) -> Dialog.show("Tint", "Is On/* omitted */", "OK", null));
hi.add(showDialog);
hi.show();
Dialog with green tinted background
Figure 59. Dialog with green tinted background

You can apply Gaussian blur to the background of a dialog to highlight the foreground further and produce an attractive effect. You can use the setDefaultBlurBackgroundRadius to apply this globally, you can use the theme constant dialogBlurRadiusInt to do the same or you can do this on a per Dialog basis using setBlurBackgroundRadius.

Not all device types support blur you can test if your device supports it using Display.getInstnace().isGaussianBlurSupported(). If blur isn’t supported the blur setting will be ignored:
Form hi = new Form("Blur Dialog", new BoxLayout(BoxLayout.Y_AXIS));
Dialog.setDefaultBlurBackgroundRadius(8);
Button showDialog = new Button("Blur");
showDialog.addActionListener((e) -> Dialog.show("Blur", "Is On/* omitted */", "OK", null));
hi.add(showDialog);
hi.show();
The blur effect coupled with the OS default tint
Figure 60. The blur effect coupled with the OS default tint

It might be a bit hard to notice the blur effect with the tinting so here is the same code with tinting disabled:

hi.setTintColor(0);
The blur effect is more pronounced when the tint is disabled
Figure 61. The blur effect is more pronounced when the tint is disabled

8.3.3. Popup dialog

A popup dialog is a common mobile paradigm showing a Dialog that points at a specific component. It’s a standard Dialog that’s shown in a unique way:

Form hi = new Form("Popup Dialog", new BoxLayout(BoxLayout.Y_AXIS));
Button showDialog = new Button("Show");
showDialog.addActionListener(e -> {
    Dialog d = new Dialog("Title");
    d.setLayout(new BorderLayout());
    d.add(BorderLayout.CENTER, new SpanLabel("Dialog Body", "DialogBody"));
    // the popup points at the button, which must be on screen to have a position
    d.showPopupDialog(showDialog);
});
hi.add(showDialog);
hi.show();
Popup Dialog
Figure 62. Popup Dialog

The popup dialog accepts a Component or Rectangle to point at and handles the rest.

Styling the arrow of the popup dialog

When Codename One was young you needed a popup arrow implementation but your low-level graphics API was pretty basic. As a workaround you created a version of the 9-piece image border that supported pointing arrows at a component.

Today Codename One supports pointing an arrow from the RoundRectBorder class. This is implicit for the PopupDialog UI. This allows for better customization of the border (color etc.) and it looks better on newer displays. It also works on all OSs. Right now the iOS theme has the old image border approach.

This will change with a future update where all OSes will align and iOS will use the lightweight popup too
You can make all OSes act the same way by overriding the PopupDialog UIID and defining its style to RoundRectBorder

The new RoundRectBorder support works by setting the track component property on border. When that’s done the border implicitly points to the right location.

If you still need deeper customization of the arrow you can still use the old 9-piece border functionality illustrated below.

Legacy 9-Piece Border arrow

One of the harder aspects of a popup dialog is the construction of the theme elements required for arrow styling. To get that sort of behavior you will need a custom image border and 4 arrows pointing in each direction that will be overlaid with the border.

The sizes of the arrow images should be similarly proportioned and fit within the image borders whitespace. The block image of the dialog should have empty pixels in the sides to reserve space for the arrow. For example: if the arrows are all 32×32 pixels then the PopupDialog image should have 32 pixels of transparent pixels around it.

You will need to define the following theme constants for the arrow to work:

PopupDialogArrowBool=true
PopupDialogArrowTopImage=arrow-up.png
PopupDialogArrowBottomImage=arrow-down.png
PopupDialogArrowLeftImage=arrow-left.png
PopupDialogArrowRightImage=arrow-right.png

Each of the four image constants names an image in your theme resource, so add your own arrows under those names — the values here are placeholders and there are no arrows built in. A constant naming an image the theme doesn’t hold leaves the border with nothing to paint.

Then style the PopupDialog UIID with the image for the Dialog itself.

8.4. InteractionDialog

Dialogs in Codename One can be modal or modeless, the former blocks the calling thread and the latter doesn’t. For example, there is another definition to those terms: A modal dialog blocks access to the rest of the UI while a modeless dialog "floats" on top of the UI.

In that sense, all dialogs in Codename One are modal; they block the parent form since they’re effectively forms that show the "parent" in their background. InteractionDialog has an API that’s like the Dialog API but, unlike dialog, it never blocks anything. Neither the calling thread nor the UI.

InteractionDialog isn’t a Dialog since it doesn’t share the same inheritance hierarchy. For example, it acts and "feels" like a Dialog although it’s a Container in the LayeredPane.

InteractionDialog is a container that’s positioned within the layered pane. Notice that because of that design, you can have one such dialog at the moment and, if you add something else to the layered pane, you might run into trouble.

Using the interaction dialog is pretty trivial and like dialog:

InteractionDialog dlg = new InteractionDialog("Hello");
dlg.setLayout(new BorderLayout());
dlg.add(BorderLayout.CENTER, new Label("Hello Dialog"));
Button close = new Button("Close");
close.addActionListener((ee) -> dlg.dispose());
dlg.addComponent(BorderLayout.SOUTH, close);
Dimension pre = dlg.getContentPane().getPreferredSize();
int displayWidth = Display.getInstance().getDisplayWidth();
int dialogWidth = Math.max(pre.getWidth() + pre.getWidth() / 6, displayWidth * 2 / 3);
dlg.show(0, 0, displayWidth - dialogWidth, 0);
Interaction Dialog
Figure 63. Interaction Dialog

This will show the dialog on the right-hand side of the screen, which is pretty useful for a floating in place dialog.

The InteractionDialog can be shown at absolute or popup locations. This is inherent to its use case which is "non-blocking." When using this component you need to be aware of its location.

To make popup behavior feel natural on touch devices you can call setDisposeWhenPointerOutOfBounds(true) so the dialog automatically dismisses as soon as the user taps outside the title or content area. Internally the dialog listens for pointer pressed/released events and will call dispose() for you when the interaction happens beyond its bounds, so you no longer need to wire that logic manually.

By default the dialog is placed on the form’s layered pane, but you can switch between the global layered pane and form-specific layered pane using setFormMode(boolean). Setting form mode to true keeps the dialog coupled with the showing form even when the global layered pane is used elsewhere in your app.

Finally, recent updates added animation toggles so you can fine-tune presentation. setAnimateShow(boolean) turns the show/dispose animations on or off, while setRepositionAnimation(boolean) enables a "grow/shrink" reposition effect during those animations when you want a more dynamic transition.

8.5. Label

Label represents a text, icon, or both. Label is also the base class of Button which in turn is the base class for RadioButton & CheckBox. Thus the functionality of the Label class extends to all these components.

Label text can be positioned in one of 4 locations as such:

Form hi = new Form("Label Position", new BoxLayout(BoxLayout.Y_AXIS));
Image icon = FontImage.createMaterial(FontImage.MATERIAL_INFO, "Label", 3.0f);
Label left = new Label("Left", icon);
left.setTextPosition(Component.LEFT);
Label right = new Label("Right", icon);
right.setTextPosition(Component.RIGHT);
Label bottom = new Label("Bottom", icon);
bottom.setTextPosition(Component.BOTTOM);
Label top = new Label("Top", icon);
top.setTextPosition(Component.TOP);
hi.add(left).add(right).add(bottom).add(top);
hi.show();
Label positions
Figure 64. Label positions

Label allows a single line of text, line breaking is an expensive operation on mobile devices [1] and so the Label class doesn’t support it.

SpanLabel supports many lines with a single label, notice that it does carry a performance penalty for this functionality.

Labels support tickering and the ability to end with "…​" if there isn’t enough space to render the label. Developers can determine the placement of the label to its icon in a few powerful ways.

8.5.1. Label gap

The gap between the label text & the icon defaults to 2 pixels due to legacy settings. The setGap method of Label accepts a gap size in pixels.

Two pixels is low for most cases & it’s hard to customize for each Label.

You can use the theme constant labelGap which is a floating point value you can specify in millimeters that will allow you to determine the default gap for a label. You can also customize this manually using the method Label.setDefaultGap(int) which determines the default gap in pixels.

8.5.2. Autosizing labels

One of the common requests you received over the years is a way to let text "fit" into the allocated space so the font will match almost the width available. In some designs this is important but it’s also tricky. Measuring the width of a String is an expensive operation on some OSes., there is no other way other than trial & error to find the "best size."

Still although something is "slow" you might still want to use it for some cases, this isn’t something you should use in a renderer, infinite scroll etc. and recommend minimizing the usage of this feature as much as possible.

This feature is applicable to Label and its subclasses (for example: Button), with components such as TextArea (for example: SpanButton) the choice between shrinking and line break would require some complex logic.

To activate this feature use setAutoSizeMode(true) for example:

Form hi = new Form("AutoSize", BoxLayout.y());

Label a = new Label("Short Text");
a.setAutoSizeMode(true);
Label b = new Label("Much Longer Text than the previous line...");
b.setAutoSizeMode(true);
Label c = new Label("MUCH MUCH MUCH Much Longer Text than the previous line by a pretty big margin...");
c.setAutoSizeMode(true);

Label a1 = new Button("Short Text");
a1.setAutoSizeMode(true);
Label b1 = new Button("Much Longer Text than the previous line...");
b1.setAutoSizeMode(true);
Label c1 = new Button("MUCH MUCH MUCH Much Longer Text than the previous line by a pretty big margin...");
c1.setAutoSizeMode(true);
hi.addAll(a, b, c, a1, b1, c1);

hi.show();
Automatically sizes the fonts of the buttons/labels based on text and available space
Figure 65. Automatically sizes the fonts of the buttons/labels based on text and available space

8.6. TextField and TextArea

The TextField class derives from the TextArea class, and both are used for text input in Codename One.

TextArea defaults to multi-line input and TextField defaults to single line input but both can be used in both cases. The main differences between TextField and TextArea are:

  • Blinking cursor is rendered on TextField

  • DataChangedListener is available in TextField. This is crucial for character by character input event tracking

  • Done listener is available in the TextField

  • Different UIID

The semantic difference between TextField & TextArea dates back to the ancestor of Codename One: LWUIT. Feature phones don’t have "proper" in-place editing capabilities & thus TextField was introduced to allow such input.

Because it lacks the blinking cursor capability TextArea is often used as a multi-line label and is used internally in SpanLabel, SpanButton etc.

A common use case is to have an important text component in edit mode as you enter a Form. Codename One forms support this exact use case through the Form.setEditOnShow(TextArea) method.

TextField & TextArea support constraints for various types of input such as NUMERIC, EMAIL, URL, etc. Those usually affect the virtual keyboard used, but might not limit input in some platforms. For example: on iOS even with NUMERIC constraint you would still be able to input characters.

If you need to prevent specific types of input check out the validation section.

The following sample shows off simple text field usage:

Form hi = new Form("Text", BoxLayout.y());
TextField plain = new TextField("", "Any text");
TextField email = new TextField("", "E-Mail", 20, TextField.EMAILADDR);
TextField url = new TextField("", "URL", 20, TextField.URL);
TextField phone = new TextField("", "Phone", 20, TextField.PHONENUMBER);
TextField number = new TextField("", "Number", 20, TextField.NUMERIC);
TextArea notes = new TextArea(4, 20);
notes.setHint("Notes");
hi.addAll(plain, email, url, phone, number, notes);
hi.show();
Simple text component sample
Figure 66. Simple text component sample
The Toolbar section contains an elaborate TextField search sample with DataChangedListener and rather unique styling.

8.6.1. Masking

A common use case when working with text components is the ability to "mask" input for example: in the credit card number above you would want 4 digits for each text field and don’t want the user to tap Next 3 times.

Masking allows you to accept partial input in one field and implicitly move to the next, this can be used to all types of complex input thanks to the text component API. E.g with the code above you can mask the credit card input so the cursor jumps to the next field implicitly using this code:

TextField num1 = new TextField("", "", 5, TextField.NUMERIC);
TextField num2 = new TextField("", "", 5, TextField.NUMERIC);
TextField num3 = new TextField("", "", 5, TextField.NUMERIC);
TextField num4 = new TextField("", "", 5, TextField.NUMERIC);
// the first three are trimmed to four by automoveToNext when the fifth
// digit arrives; the last one has no successor, so it needs a limit
num4.setMaxSize(4);
automoveToNext(num1, num2);
automoveToNext(num2, num3);
automoveToNext(num3, num4);

Then implement the method automoveToNext as:

private void automoveToNext(final TextField current, final TextField next) {
    current.addDataChangedListener((type, index) -> {
        String val = current.getText();
        // more than one digit can arrive at once from a paste or autofill, so
        // keep four and hand the whole remainder on -- the next field's own
        // listener splits it again if it is still too long
        if(val.length() > 4) {
            current.stopEditing();
            current.setText(val.substring(0, 4));
            String rest = val.substring(4);
            next.setText(rest);
            if(rest.length() <= 4) {
                // a longer remainder means the next field's listener is still
                // splitting; let the last one in the chain take the focus
                next.startEditingAsync();
            }
        }
    });
}

Notice you can invoke stopEditing(Runnable) where you receive a callback as editing is stopped.

8.6.2. The virtual keyboard

A common misconception for developers is assuming the virtual keyboard represents "keys." For example: developers often override the "keyEvent" callbacks which are invoked for physical keyboard typing and expect those to occur with a virtual keyboard.

This isn’t the case since a virtual keyboard is a different beast. With a virtual keyboard characters typed might produce a different output due to autocorrect. Some keyboards don’t even have "keys" in the traditional sense or don’t type them in the traditional sense (for example: swiping).

The constraint property for the TextField/TextArea is crucial for a virtual keyboard.
When working with a virtual keyboard it’s important that the parent Container for the TextField/TextArea is scrollable. Otherwise the component won’t be reachable or the UI might be distorted when the keyboard appears.
Action Button client property

By default, the virtual keyboard on Android has a "Done" button, you can customize it to be a search icon, a send icon, or a go icon using a hint such as this:

searchTextField.putClientProperty("searchField", Boolean.TRUE);
sendTextField.putClientProperty("sendButton", Boolean.TRUE);
goTextField.putClientProperty("goButton", Boolean.TRUE);

This will adapt the icon for the action on the keys.

Next and done on iOS

You try to hide a lot of the platform differences in Codename One, input is ** different between OSes. A common reliance is the ability to send the "Done" event when the user presses the Done button. This button doesn’t always exist for example: if there is an Enter button (due to multiline input) or if there is a Next button in that place.

To unify the behavior you slightly customized the iOS keyboard as such:

Next virtual keyboard with toolbar
Figure 67. Next virtual keyboard with toolbar
Done virtual keyboard without toolbar
Figure 68. Done virtual keyboard without toolbar
This works with 3rd party keyboards too…​

For example, this behavior might not be desired so to block that you can do:

tf.putClientProperty("iosHideToolbar", Boolean.TRUE);

This will hide the toolbar for that given field.

You can customize the color of the Done button in the toolbar by setting the ios.doneButtonColor display property. For example: To change the color to red, you could do Display.getInstance().setProperty("ios.doneButtonColor", String.valueOf(0xff0000)).

8.6.3. Clearable text field

iOS has a convention where an X can be placed after the text field to clear it. Some Android apps have it but there is no native support for that as of this writing.

You can wrap a TextField with a clearable wrapper to get this effect on all platforms. For example: replace this:

cnt.add(myTextField);

With this:

cnt.add(ClearableTextField.wrap(myTextField));

You can also specify the size of the clear icon if you wish. This is technically a Container with the text field style and a button to clear the text at the edge.

8.7. TextComponent

When building input forms you sometimes want to adapt to the native OS behavior and create a UI that’s a bit more distinct to the native OS. TextField and TextArea are low-level, you can create an Android style UI with such components but it might look out of place in iOS.

For example: this is how most of you would expect the UI to look on iOS and Android respectively:

TextModeLayout on iOS
Figure 69. Text Input on iOS
TextModeLayout on Android with the same code
Figure 70. Text Input on Android

Doing this with text fields is possible but would require code that looks a bit different and jumps through hoops. TextComponent allows this exact UI without forcing developers to write OS specific code:

TextModeLayout tl = new TextModeLayout(3, 2);
Form f = new Form("Pixel Perfect", tl);
TextComponent title = new TextComponent().label("Title");
TextComponent price = new TextComponent().label("Price");
TextComponent location = new TextComponent().label("Location");
TextComponent description = new TextComponent().label("Description").multiline(true);

f.add(tl.createConstraint().horizontalSpan(2), title);
f.add(tl.createConstraint().widthPercentage(30), price);
f.add(tl.createConstraint().widthPercentage(70), location);
f.add(tl.createConstraint().horizontalSpan(2), description);
f.setEditOnShow(title.getField());
f.show();
This code uses the TextModeLayout which is discussed in the layouts section

The text component uses a builder approach to set various values for example:

TextComponent t = new TextComponent().
    text("This appears in the text field").
    hint("This is the hint").
    label("This is the label").
    multiline(true);

The code is pretty self-explanatory and more convenient than typical setters/getters. It automatically handles the floating hint style of animation when running on Android.

8.7.1. Error handling

The validator class supports text component and it should "work." But the cool thing is that it uses the material design convention for error handling!

If you add to the sample above a Validator:

Validator val = new Validator();
val.addConstraint(title, new LengthConstraint(2));
val.addConstraint(price, new NumericConstraint(true));

You would see something that looks like this on Android:

Error handling when the text is blank
Figure 71. Error handling when the text is blank
Error handling when there is some input (notice red title label)
Figure 72. Error handling when there is some input (notice red title label)
On iOS the situation hasn’t changed much yet
Figure 73. On iOS the situation hasn’t changed much yet

The underlying system is the errorMessage method which you can chain like the other methods on TextComponent as such:

TextComponent tc = new TextComponent().
    label("Input Required").
    errorMessage("Input is essential in this field");

8.7.2. InputComponent and PickerComponent

To keep the code common and generic you use the InputComponent abstract base class and derive the other classes from that. PickerComponent is the other option.

A picker can work with your existing sample using code like this:

TextModeLayout tl = new TextModeLayout(3, 2);
Form f = new Form("Pixel Perfect", tl);
TextComponent title = new TextComponent().label("Title");
TextComponent price = new TextComponent().label("Price");
TextComponent location = new TextComponent().label("Location");
PickerComponent date = PickerComponent.createDate(new Date()).label("Date");
TextComponent description = new TextComponent().label("Description").multiline(true);
Validator val = new Validator();
val.addConstraint(title, new LengthConstraint(2));
val.addConstraint(price, new NumericConstraint(true));
f.add(tl.createConstraint().widthPercentage(60), title);
f.add(tl.createConstraint().widthPercentage(40), date);
f.add(location);
f.add(price);
f.add(tl.createConstraint().horizontalSpan(2), description);
f.setEditOnShow(title.getField());
f.show();

This produces the following which looks pretty standard:

Picker component taking place in iOS
Figure 74. Picker component taking place in iOS
And in Android
Figure 75. And in Android

The one tiny thing you should notice with the PickerComponent is that you don’t construct the picker component using new PickerComponent(). Instead you use create methods such as PickerComponent.createDate(new Date()). The reason for that’s that you have many types of pickers and it wouldn’t make sense to have one constructor.

8.7.3. Underlying theme constants and UIID’s

These varying looks are implemented through a combination of layouts, theme constants and UIID’s. The most important UIID’s are: TextComponent, FloatingHint & TextHint.

There are many related theme constants that can manipulate some pieces of this functionality:

  • textComponentErrorColor a hex RGB color which defaults to null in which case this has no effect. When defined this will change the color of the border and label to the given color to match the material design styling. This implements the red border underline in cases of error and the label text color change

  • textComponentErrorLineBorderBool toggles the material-style underline that appears on validation errors. Set it to false if you prefer to supply a different border when errors are shown

  • textComponentOnTopBool toggles the on top mode which makes things look like they do on Android. This defaults to true on Android and false on other OSes. This can also be manipulated through the onTopMode(boolean) method in InputComponent, but the layout will use the theme constant

  • textComponentAnimBool toggles the animation mode which again can be manipulated by a method in InputComponent. If you want to keep the UI static without the floating hint effect set this to false. Notice this defaults to true on Android

  • textComponentFieldUIID sets the UIID of the text field to something other than TextField this is useful for platforms such as iOS where the look of the text field is different within the text component. This allows you to make the background of the text field transparent when it’s within the TextComponent and make it different from the regular text field

  • inputComponentErrorMultilineBool makes error labels multi-line by default so longer validation messages can wrap instead of being clipped

8.8. Button

Button is a subclass of Label and as a result it inherits all its functionality, specifically icon placement, tickering, etc.

Button adds to the mix some more states such as a pressed UIID state and pressed icon.

Button supports more icon states such as rollover and disabled icon.

Button also exposes some functionality for subclasses specifically the setToggle method call which has no meaning when invoked on a Button but has a lot of implications for CheckBox & RadioButton.

Button event handling can be performed through an ActionListener or through a Command.

Changes in a Command won’t be reflected into the Button after the command was set to the Button.

Here is a trivial hello world style Button:

Form hi = new Form("Button");
Button b = new Button("My Button");
hi.add(b);
b.addActionListener((e) -> Log.p("Clicked"));

The same three buttons under the two modern native themes. A button takes its shape, fill and font from the theme, so the platform difference costs no code:

The same buttons under iOS Modern (left) and Android Material (right)

iOS Modern Android Material

Such a button can be styled to look like a link using code like this or by making these settings in the theme and using code such as btn.setUIID("Hyperlink"):

Form hi = new Form("Button");
Button b = new Button("Link Button");
b.getAllStyles().setBorder(Border.createEmpty());
b.getAllStyles().setTextDecoration(Style.TEXT_DECORATION_UNDERLINE);
hi.add(b);
b.addActionListener((e) -> Log.p("Clicked"));
Button styled to look like a link
Figure 76. Button styled to look like a link

8.8.1. Uppercase buttons

Buttons on Android’s material design UI use upper case styling which isn’t the case for iOS. To solve this you have the method setCapsText(boolean) in Button which has the corresponding isCapsText, isCapsTextDefault & setCapsTextDefault(boolean). This is pretty core to Codename One so to prevent this from impacting everything unless you explicitly invoke setCapsText(boolean) the default value of true will apply when the UIID is Button, RaisedButton or for the built-in Dialog buttons.

You also have a theme constant: capsButtonTextBool. This constant controls caps text behavior from the theme and is set to true in the Android native theme.

8.8.2. Raised Button

Raised button is a style of button that’s available on Android and used to highlight an important action within a form. To confirm with the material design UI guidelines you might want to leverage a raised button UI element on Android but use a regular button everywhere else.

First you need to know whether a raised button exists in the theme. On Android this will return true but on other OSes it will return false. A potential future update might make another platform true based on UI guidelines in other OSes.

For this purpose you have got the theme constant hasRaisedButtonBool which will return true on Android but will be false elsewhere. You can use it like this:

if(UIManager.getInstance().isThemeConstant("hasRaisedButtonBool", false)) {
    // that means we can use a raised button
}

To enable this you have the RaisedButton UIID that derives from Button and will act like it except for the places where hasRaisedButtonBool is true in which case it will look like this:

Raised and flat button in simulator
Figure 77. Raised and flat button in simulator

Notice that you can customize the colors of these buttons now since the border respects user colors…​

In this case set the background color to purple and the foreground to white:

Purple raised button
Figure 78. Purple raised button
Form f = new Form("Pixel Perfect", BoxLayout.y());
Button b = new Button("Raised Button", "RaisedButton");
Button r = new Button("Flat Button");
f.add(b);
f.add(r);
f.show();

8.8.3. Ripple effect

The ripple effect in material design highlights the location of the finger and grows as a circle to occupy the full area of the component as the user presses the button.

You can perform a ripple effect by darkening the touched area and growing that in a quick animation.

Ripple effect can be applied to any component but you have it turned on for buttons on Android which also applies to things like title commands, side menu elements etc. This might not apply at this moment to lead components like multi-buttons but that might change in the future.

Component has a property to enable the ripple effect setRippleEffect(boolean) and the corresponding isRippleEffect(). You can turn it on or off individually in the component level. For example, Button has static setButtonRippleEffectDefault(boolean) and isButtonRippleEffectDefault(). These allow you to define the default behavior for all the buttons and that can be configured through the theme constant buttonRippleBool which is on by default on the native Android theme.

8.9. CheckBox/RadioButton

CheckBox & RadioButton are subclasses of button that allow for either a toggle state or exclusive selection state.

Both CheckBox & RadioButton have a selected state that allows you to determine their selection.

RadioButton doesn’t allow you to "deselect" it, the way to "deselect" a RadioButton is by selecting another RadioButton.

The CheckBox can be added to a Container like any other Component but the RadioButton must be associated with a ButtonGroup otherwise if you have more than one set of RadioButton’s in the form you might have an issue.

Notice in the sample below that you associate all the radio buttons with a group but don’t do anything with the group as the radio buttons keep the reference internally. You also show the opposite side functionality and icon behavior:

Form hi = new Form("CheckBox", new BoxLayout(BoxLayout.Y_AXIS));
Image icon = FontImage.createMaterial(FontImage.MATERIAL_INFO, "Label", 3.0f);
CheckBox cb1 = new CheckBox("CheckBox No Icon");
cb1.setSelected(true);
CheckBox cb2 = new CheckBox("CheckBox With Icon", icon);
CheckBox cb3 = new CheckBox("CheckBox Opposite True", icon);
CheckBox cb4 = new CheckBox("CheckBox Opposite False", icon);
cb3.setOppositeSide(true);
cb4.setOppositeSide(false);
RadioButton rb1 = new RadioButton("Radio 1");
RadioButton rb2 = new RadioButton("Radio 2");
RadioButton rb3 = new RadioButton("Radio 3", icon);
new ButtonGroup(rb1, rb2, rb3);
rb2.setSelected(true);
hi.add(cb1).add(cb2).add(cb3).add(cb4).add(rb1).add(rb2).add(rb3);
hi.show();
RadioButton & CheckBox usage
Figure 79. RadioButton & CheckBox usage

Both of these components can be displayed as toggle buttons (see the toggle button section below), or use the default check mark/filled circle appearance based on the type/OS.

8.9.1. Toggle Button

A toggle button is a button that’s pressed and stays pressed. When a toggle button is pressed again it’s released from the pressed state. Hence the button has a selected state to show if it’s pressed or not like the CheckBox/RadioButton components in Codename One.

To turn any CheckBox or RadioButton to a toggle button use the setToggle(true) method. Or you can use the static createToggle method on both CheckBox and RadioButton to create a toggle button directly.

Invoking setToggle(true) implicitly converts the UIID to ToggleButton unless it was changed by the user from its original default value.

You can convert the sample above to use toggle buttons as such:

Form hi = new Form("RadioButton", new BoxLayout(BoxLayout.Y_AXIS));
Image icon = FontImage.createMaterial(FontImage.MATERIAL_INFO, "Label", 3.0f);
CheckBox cb1 = CheckBox.createToggle("CheckBox No Icon");
cb1.setSelected(true);
CheckBox cb2 = CheckBox.createToggle("CheckBox With Icon", icon);
CheckBox cb3 = CheckBox.createToggle("CheckBox Opposite True", icon);
CheckBox cb4 = CheckBox.createToggle("CheckBox Opposite False", icon);
cb3.setOppositeSide(true);
cb4.setOppositeSide(false);
ButtonGroup bg = new ButtonGroup();
RadioButton rb1 = RadioButton.createToggle("Radio 1", bg);
RadioButton rb2 = RadioButton.createToggle("Radio 2", bg);
RadioButton rb3 = RadioButton.createToggle("Radio 3", icon, bg);
rb2.setSelected(true);
hi.add(cb1).add(cb2).add(cb3).add(cb4).add(rb1).add(rb2).add(rb3);
hi.show();
Toggle button converted sample, on Android Material and iOS

Toggle buttons under Android Material Toggle buttons under the iOS theme

Both themes draw the selection as a change of fill rather than of label color alone, so a selected toggle stays readable for someone who can’t separate the two hues. The outlined row in each figure is the one holding focus: focus is drawn as a ring precisely so it can’t be mistaken for a value, since the framework resolves a focused toggle through the same style whether it’s checked or not.

That’s half the story though: to get the full effect of some cool toggle button UI’s you can use a ComponentGroup. This allows you to create a button bar effect with the toggle buttons.

For example, to enclose the CheckBox components in a vertical ComponentGroup and the RadioButton’s in a horizontal group, change the last line of the code above as such:

Form hi = new Form("ComponentGroup", new BoxLayout(BoxLayout.Y_AXIS));
CheckBox cb1 = CheckBox.createToggle("CheckBox 1");
CheckBox cb2 = CheckBox.createToggle("CheckBox 2");
CheckBox cb3 = CheckBox.createToggle("CheckBox 3");
CheckBox cb4 = CheckBox.createToggle("CheckBox 4");
ButtonGroup bg = new ButtonGroup();
RadioButton rb1 = RadioButton.createToggle("Radio 1", bg);
RadioButton rb2 = RadioButton.createToggle("Radio 2", bg);
RadioButton rb3 = RadioButton.createToggle("Radio 3", bg);
hi.add(ComponentGroup.enclose(cb1, cb2, cb3, cb4)).
        add(ComponentGroup.encloseHorizontal(rb1, rb2, rb3));
hi.show();
Toggle button converted sample wrapped in ComponentGroup
Figure 80. Toggle button converted sample wrapped in ComponentGroup

8.10. ComponentGroup

ComponentGroup is a special container that can be either horizontal or vertical (BoxLayout X_AXIS or Y_AXIS respectively).

ComponentGroup "restyles" the elements within the group to have a UIID that allows you to create a "round border" effect that groups elements together.

The following code adds 4 component groups to a Container to show the various UIID changes:

hi.add("Three Labels").
        add(ComponentGroup.enclose(new Label("GroupElementFirst UIID"), new Label("GroupElement UIID"), new Label("GroupElementLast UIID"))).
        add("One Label").
        add(ComponentGroup.enclose(new Label("GroupElementOnly UIID"))).
        add("Three Buttons").
        add(ComponentGroup.enclose(new Button("ButtonGroupFirst UIID"), new Button("ButtonGroup UIID"), new Button("ButtonGroupLast UIID"))).
        add("One Button").
        add(ComponentGroup.enclose(new Button("ButtonGroupOnly UIID")));
ComponentGroup adapts the UIID’s of the components added so you can style them
Figure 81. ComponentGroup adapts the UIID’s of the components added so you can style them

Notice the following about the code above and the resulting image:

  • Buttons have a different UIID than other element types. Their styling is slightly different in such UI’s so you need to pay attention to that.

  • When an element is placed alone within a ComponentGroup its a special case UIID.

A vertical ComponentGroup does nothing unless the theme sets the ComponentGroupBool constant to true; without it the group is just a box layout container. The legacy iOS themes set that flag, the current native themes don’t, and neither of them styles the GroupElement UIIDs a vertical group applies. A horizontal group is the exception, as long as it keeps the UIID setHorizontal gives it: it’s a segmented control, it renames its members to ToggleButton, and both native themes style that UIID and its First / Last / Only variants in full, so it needs no constant. Name your own element UIID with setElementUIID and it’s theme-gated again, exactly like a vertical one.

When ComponentGroupBool is set to true, the component group will change the styles of all components placed within it to match the element UIID given to it (GroupElement by default) with special caveats to the first/last/ elements. For example:

  1. With one element in a component group it will have the UIID: GroupElementOnly

  2. With two elements in a component group they will have the UIID’s GroupElementFirst, GroupElementLast

  3. With three elements in a component group they will have the UIID’s GroupElementFirst, GroupElement, GroupElementLast

  4. With four elements in a component group they will have the UIID’s GroupElementFirst, GroupElement, GroupElement, GroupElementLast

This allows you to define special styles for the edges.

You can customize the UIID set by the component group by calling setElementUIID in the component group for example: setElementUIID("ToggleButton") for three elements result in the following UIID’s:

ToggleButtonFirst, ToggleButton, ToggleButtonLast

8.11. MultiButton

MultiButton is a composite component (lead component) that acts like a versatile Button. It supports up to 4 lines of text (it doesn’t automatically wrap the text), an emblem (usually navigational arrow, or check box) and an icon.

MultiButton can be used as a button, a CheckBox or a RadioButton for creating rich UI’s.

The MultiButton was inspired by the aesthetics of the UITableView iOS component.

A common source of confusion in the MultiButton is the difference between the icon and the emblem, since both may have an icon image associated with them. The icon is an image representing the entry while the emblem is an optional visual representation of the action that will be undertaken when the element is pressed. Both may be used simultaneously or individually of one another:

Form hi = new Form("MultiButton", new BoxLayout(BoxLayout.Y_AXIS));
Image icon = FontImage.createMaterial(FontImage.MATERIAL_INFO, "Label", 3.0f);
Image emblem = FontImage.createMaterial(FontImage.MATERIAL_STAR, "Label", 2.0f);
MultiButton twoLinesNoIcon = new MultiButton("MultiButton");
twoLinesNoIcon.setTextLine2("Line 2");
MultiButton oneLineIconEmblem = new MultiButton("Icon + Emblem");
oneLineIconEmblem.setIcon(icon);
oneLineIconEmblem.setEmblem(emblem);
MultiButton twoLinesIconEmblem = new MultiButton("Icon + Emblem");
twoLinesIconEmblem.setIcon(icon);
twoLinesIconEmblem.setEmblem(emblem);
twoLinesIconEmblem.setTextLine2("Line 2");

MultiButton twoLinesIconEmblemHorizontal = new MultiButton("Icon + Emblem");
twoLinesIconEmblemHorizontal.setIcon(icon);
twoLinesIconEmblemHorizontal.setEmblem(emblem);
twoLinesIconEmblemHorizontal.setTextLine2("Line 2 Horizontal");
twoLinesIconEmblemHorizontal.setHorizontalLayout(true);

MultiButton twoLinesIconCheckBox = new MultiButton("CheckBox");
twoLinesIconCheckBox.setIcon(icon);
twoLinesIconCheckBox.setCheckBox(true);
twoLinesIconCheckBox.setTextLine2("Line 2");

MultiButton fourLinesIcon = new MultiButton("With Icon");
fourLinesIcon.setIcon(icon);
fourLinesIcon.setTextLine2("Line 2");
fourLinesIcon.setTextLine3("Line 3");
fourLinesIcon.setTextLine4("Line 4");

hi.add(oneLineIconEmblem).
add(twoLinesNoIcon).
add(twoLinesIconEmblem).
add(twoLinesIconEmblemHorizontal).
add(twoLinesIconCheckBox).
add(fourLinesIcon);
hi.show();
Multiple usage scenarios for the MultiButton
Figure 82. Multiple usage scenarios for the MultiButton

8.11.1. Styling the MultiButton

Since the MultiButton is a composite component setting its UIID will impact the top level UI.

To customize everything you need to customize the UIID’s for MultiLine1, MultiLine2, MultiLine3, MultiLine4 & Emblem.

You can customize the individual UIID’s through the API directly using the setIconUIID, setUIIDLine1, setUIIDLine2, setUIIDLine3, setUIIDLine4 & setEmblemUIID.

Recent versions also include a badge overlay that can be rendered in the corner of the main icon. Use setBadgeText() to display a value (for example a notification count) and setBadgeUIID() if you need a custom UIID instead of the default Badge styling. When you need to inspect or adjust the badge style programmatically, getBadgeStyleComponent() returns the component whose styles are applied to the badge so you can tweak padding, colors or borders before showing the MultiButton.

8.12. SpanButton

SpanButton is a composite component (lead component) that looks/acts like a Button but can break lines rather than crop them when the text is long.

Unlike the MultiButton it uses the TextArea internally to break lines seamlessly. The SpanButton is far simpler than the MultiButton and as a result isn’t as configurable:

Form hi = new Form("SpanButton", new BoxLayout(BoxLayout.Y_AXIS));
Image icon = FontImage.createMaterial(FontImage.MATERIAL_INFO, "Label", 3.0f);
SpanButton sb = new SpanButton("SpanButton is a composite component (lead component) that looks/acts like a Button but can break lines rather than crop them when the text is very long.");
sb.setIcon(icon);
hi.add(sb);
hi.show();
The SpanButton Component
Figure 83. The SpanButton Component
SpanButton is slower than both Button and MultiButton. Recommend using it when there is a genuine need for its functionality.

8.13. SpanLabel

SpanLabel is a composite component (lead component) that looks/acts like a Label but can break lines rather than crop them when the text is long.

SpanLabel uses the TextArea internally to break lines seamlessly and so doesn’t provide all the elaborate configuration options of Label.

One of the features of label that moved into SpanLabel to some extent is the ability to position the icon. For example, unlike a Label the icon position is determined by the layout manager of the composite so setIconPosition accepts a BorderLayout constraint:

Form hi = new Form("SpanLabel", new BoxLayout(BoxLayout.Y_AXIS));
Image icon = FontImage.createMaterial(FontImage.MATERIAL_INFO, "Label", 3.0f);
SpanLabel d = new SpanLabel("Default SpanLabel that can seamlessly line break when the text is really long.");
d.setIcon(icon);
SpanLabel l = new SpanLabel("NORTH Positioned Icon SpanLabel that can seamlessly line break when the text is really long.");
l.setIcon(icon);
l.setIconPosition(BorderLayout.NORTH);
SpanLabel r = new SpanLabel("SOUTH Positioned Icon SpanLabel that can seamlessly line break when the text is really long.");
r.setIcon(icon);
r.setIconPosition(BorderLayout.SOUTH);
SpanLabel c = new SpanLabel("EAST Positioned Icon SpanLabel that can seamlessly line break when the text is really long.");
c.setIcon(icon);
c.setIconPosition(BorderLayout.EAST);
hi.add(d).add(l).add(r).add(c);
hi.show();
The SpanLabel Component
Figure 84. The SpanLabel Component
SpanLabel is slower than Label. Recommend using it when there is a genuine need for its functionality.

8.14. OnOffSwitch

The OnOffSwitch allows you to write an application where the user can swipe a switch between two states (on/off). This is a common UI paradigm in Android and iOS, although it’s implemented in a radically different way in both platforms.

This is a rather elaborate component because of its unique design on iOS, but you’re able to accommodate most of the small behaviors of the component into your version, and it seamlessly adapts between the Android style and the iOS style.

The image below was generated based on the default use of the OnOffSwitch:

OnOffSwitch onOff = new OnOffSwitch();
hi.add(onOff);
The OnOffSwitch component as it appears on/off on iOS (top) and on Android (bottom)
Figure 85. The OnOffSwitch component as it appears on/off on iOS (top) and on Android (bottom)

As you can understand the difference between the way iOS and Android render this component has triggered two different implementations within a single component. The Android implementation uses standard buttons and is the default for non-iOS platforms.

You can force the Android or iOS mode by using the theme constant onOffIOSModeBool.

8.14.1. Validation

Validation is an inherent part of text input, and the Validator class allows that. You can enable validation through the Validator class to add constraints for a specific component. It’s also possible to define components that would be enabled/disabled based on validation state and the way in which validation errors are rendered (change the components UIID, paint an emblem on top, etc.). A Constraint is an interface that represents validation requirements. You can define a constraint in Java or use some built-in constraints such as LengthConstraint, RegexConstraint, etc.

This sample below continues from the place where the TextField sample above stopped by adding validation to that code:

TextField firstName = new TextField("", "First Name");
TextField surname = new TextField("", "Surname");
TextField url = new TextField("", "URL", 20, TextField.URL);
TextField email = new TextField("", "E-Mail", 20, TextField.EMAILADDR);
TextField phone = new TextField("", "Phone", 20, TextField.PHONENUMBER);
String phoneRegex = "[0-9\\-\\+ ]+";
TextField num1 = new TextField("", "", 5, TextField.NUMERIC);
TextField num2 = new TextField("", "", 5, TextField.NUMERIC);
TextField num3 = new TextField("", "", 5, TextField.NUMERIC);
TextField num4 = new TextField("", "", 5, TextField.NUMERIC);
Button submit = new Button("Submit");
// the masking from the previous sample still applies to these fields
automoveToNext(num1, num2);
automoveToNext(num2, num3);
automoveToNext(num3, num4);
num4.setMaxSize(4);

Validator v = new Validator();
v.addConstraint(firstName, new LengthConstraint(2)).
        addConstraint(surname, new LengthConstraint(2)).
        addConstraint(url, RegexConstraint.validURL()).
        addConstraint(email, RegexConstraint.validEmail()).
        addConstraint(phone, new RegexConstraint(phoneRegex, "Must be valid phone number")).
        addConstraint(num1, new LengthConstraint(4)).
        addConstraint(num2, new LengthConstraint(4)).
        addConstraint(num3, new LengthConstraint(4)).
        addConstraint(num4, new LengthConstraint(4));

v.addSubmitButtons(submit);

Form hi = new Form("Validation", new BoxLayout(BoxLayout.Y_AXIS));
hi.add(firstName).add(surname).add(url).add(email).add(phone).
        add(num1).add(num2).add(num3).add(num4).add(submit);
hi.show();
Validation and Regular Expressions
Figure 86. Validation & Regular Expressions

When the form components are bound to a model with @Bindable, the same constraints can be expressed as field annotations — @Required, @Length, @Regex, @Email, @Url, @Numeric, @ExistIn, @Validate — and the generated binder wires them into a Validator exposed through Binding#getValidator(). See the Validation annotations section in the component binding chapter for the full reference.

8.15. InfiniteProgress

The InfiniteProgress indicator spins an image infinitely to show that a background process is still working.

This style of animation is often nicknamed "washing machine" as it spins endlessly.

InfiniteProgress can be used in one of two ways either by embedding the component into the UI through something like this:

myContainer.add(new InfiniteProgress());

InfiniteProgress can also appear over the entire screen, thus blocking all input. This tints the background while the infinite progress rotates:

Dialog ip = new InfiniteProgress().showInifiniteBlocking();

// do some long operation here using invokeAndBlock or do something in a separate thread and callback later
// when you are done just call

ip.dispose();
Infinite progress
Figure 87. Infinite progress

The image used in the InfiniteProgress animation is defined by the native theme. You can override that definition either by defining the theme constant infiniteImage or by invoking the setAnimation method.

Despite the name of the method setAnimation expects a static image that will be rotated internally. Don’t use an animated image.

8.16. InfiniteScrollAdapter and InfiniteContainer

InfiniteScrollAdapter & InfiniteContainer allow you to create a scrolling effect that "never" ends with the typical Container/Component paradigm.

The motivation behind these classes is simple, say you have a lot of data to fetch from storage or from the internet. You can fetch the data in batches and show progress sign while you do this.

Infinite scroll fetches the next batch of data dynamically as you reach the end of the Container. InfiniteScrollAdapter & InfiniteContainer represent two similar ways to do that task.

Let start by exploring how you can achieve this UI that fetches data from a webservice:

InfiniteScrollAdapter demo code fetching property cross data
Figure 88. InfiniteScrollAdapter demo code fetching property cross data

The first step is creating the webservice call, you won’t go into too much detail here as webservices & IO are discussed later in the guide:

int pageNumber = 1;
java.util.List<Map<String, Object>> fetchPropertyData(String text) {
    try {
        ConnectionRequest r = new ConnectionRequest();
        r.setPost(false);
        r.setUrl("https://api.nestoria.co.uk/api");
        r.addArgument("pretty", "0");
        r.addArgument("action", "search_listings");
        r.addArgument("encoding", "json");
        r.addArgument("listing_type", "buy");
        r.addArgument("page", "" + pageNumber);
        pageNumber++;
        r.addArgument("country", "uk");
        r.addArgument("place_name", text);
        NetworkManager.getInstance().addToQueueAndWait(r);
        Map<String,Object> result = new JSONParser().parseJSON(new InputStreamReader(new ByteArrayInputStream(r.getResponseData()), "UTF-8"));
        Map<String, Object> response = (Map<String, Object>)result.get("response");
        return (java.util.List<Map<String, Object>>)response.get("listings");
    } catch(Exception err) {
        Log.e(err);
        return null;
    }
}
The demo code here doesn’t do any error handling! This is a bad practice and it’s taken here to keep the code short and readable. Proper error handling is used in the Property Cross demo.

The fetchPropertyData is a simplistic tool that fetches the next page of listings for the nestoria webservice. Notice that this method is synchronous and will block the calling thread (legally) until the network operation completes.

Now that you have a webservice lets proceed to create the UI. Check out the code annotations below:

Form hi = new Form("InfiniteScrollAdapter", new BoxLayout(BoxLayout.Y_AXIS));

Style s = UIManager.getInstance().getComponentStyle("MultiLine1");
FontImage p = FontImage.createMaterial(FontImage.MATERIAL_PORTRAIT, s);
EncodedImage placeholder = EncodedImage.createFromImage(p.scaled(p.getWidth() * 3, p.getHeight() * 3), false); (1)

InfiniteScrollAdapter.createInfiniteScroll(hi.getContentPane(), () -> { (2)
    java.util.List<Map<String, Object>> data = fetchPropertyData("Leeds"); (3)
    if(data == null) { // the fetch failed, so there is nothing more to add
        InfiniteScrollAdapter.addMoreComponents(hi.getContentPane(), new Component[0], false);
        return;
    }
    MultiButton[] cmps = new MultiButton[data.size()];
    for(int iter = 0 ; iter < cmps.length ; iter++) {
        Map<String, Object> currentListing = data.get(iter);
        if(currentListing == null) { (4)
            InfiniteScrollAdapter.addMoreComponents(hi.getContentPane(), new Component[0], false);
            return;
        }
        String thumb_url = (String)currentListing.get("thumb_url");
        String guid = (String)currentListing.get("guid");
        String summary = (String)currentListing.get("summary");
        cmps[iter] = new MultiButton(summary);
        cmps[iter].setIcon(URLImage.createToStorage(placeholder, guid, thumb_url));
    }
    InfiniteScrollAdapter.addMoreComponents(hi.getContentPane(), cmps, !data.isEmpty()); (5)
}, true); (6)
hi.show();
1Placeholder is essential for the URLImage class which this guide covers at a different place.
2The InfiniteScrollAdapter accepts a runnable which is invoked every time you reach the edge of the scrolling. You used a closure instead of the typical run() method override.
3This is a blocking call, after the method completes you will have all the data you need. Notice that this method doesn’t block the EDT illegally.
4If there is no more data you call the addMoreComponents method with a false argument. This indicates that there is no more data to fetch.
5Here you add the actual components to the end of the form. Notice that you must not invoke the add/remove method of Container. Those might conflict with the work of the InfiniteScrollAdapter.
6You pass true to show that the data isn’t "prefilled" so the method should be invoked when the Form is first shown
Don’t violate the EDT in the callback. It’s invoked on the event dispatch thread and it’s crucial

8.16.1. The InfiniteContainer

InfiniteContainer was introduced to simplify and remove some boilerplate of the InfiniteScrollAdapter. It takes a more traditional approach of inheriting the Container class to provide its functionality.

Unlike the InfiniteScrollAdapter the InfiniteContainer accepts an index and amount to fetch. This is useful for tracking your position but also important since the InfiniteContainer also implements Pull To Refresh as part of its functionality.

Converting the code above to an InfiniteContainer is pretty simple you moved all the code into the callback fetchComponents method and returned the array of Component’s as a response.

Unlike the InfiniteScrollAdapter you can’t use the ContentPane directly so you’ve to use a BorderLayout and place the InfiniteContainer there:

Form hi = new Form("InfiniteContainer", new BorderLayout());

Style s = UIManager.getInstance().getComponentStyle("MultiLine1");
FontImage p = FontImage.createMaterial(FontImage.MATERIAL_PORTRAIT, s);
EncodedImage placeholder = EncodedImage.createFromImage(p.scaled(p.getWidth() * 3, p.getHeight() * 3), false);

InfiniteContainer ic = new InfiniteContainer() {
    @Override
    public Component[] fetchComponents(int index, int amount) {
        if(index == 0) {
            // a pull to refresh asks for the start of the list again
            pageNumber = 1;
        }
        java.util.List<Map<String, Object>> data = fetchPropertyData("Leeds");
    if(data == null) { // the fetch failed, so there is nothing more to add
        return null;
    }
        MultiButton[] cmps = new MultiButton[data.size()];
        for(int iter = 0 ; iter < cmps.length ; iter++) {
            Map<String, Object> currentListing = data.get(iter);
            if(currentListing == null) {
                return null;
            }
            String thumb_url = (String)currentListing.get("thumb_url");
            String guid = (String)currentListing.get("guid");
            String summary = (String)currentListing.get("summary");
            cmps[iter] = new MultiButton(summary);
            cmps[iter].setIcon(URLImage.createToStorage(placeholder, guid, thumb_url));
        }
        return cmps;
    }
};
hi.add(BorderLayout.CENTER, ic);
hi.show();

8.17. List, MultiList, renderers & models

8.17.1. InfiniteContainer/InfiniteScrollAdapter vs. List/ContainerList

The recommendation is to always go with Container, InfiniteContainer or InfiniteScrollAdapter.

Recommend avoiding List or its subclasses/related classes specifically ContainerList & MultiList.

Recommend replacing ComboBox with Picker but that’s a different discussion.

A Container with ~5000 nested containers within it can perform on par with a List and probably exceed its performance when used.

Larger sets of data are manageable on phones or tablets so the benefits for lists are dubious.

In API you found that even experienced developers experienced a great deal of pain when wrangling the Swing styled lists and their stateless approach.

Since animation, swiping and other capabilities that are so common in mobile are so hard to do with lists you see no actual reason to use them.

8.17.2. Why isn’t list deprecated

ContainerList is deprecated because it performs badly and has some inherent complexity issues. List has some unique use cases and is still used all over Codename One.

MultiList is a reasonable version of List that’s far easier to use without most of the pains related to renderer configuration.

Some cases justify using List or MultiList, but they’re rarer than usual, hence the recommendation.

8.17.3. MVC in lists

A Codename One List doesn’t contain components, but rather arbitrary data; this seems odd at first but makes sense. If you want a list to contain components, use a Container.

The advantage of using a List in this way is that you can display it in many ways (for example: fixed focus positions, horizontally, etc.), and that you can have more than a million entries without performance overhead. You can also do some pretty nifty things, like filtering the list on the fly or fetching it dynamically from the Internet as the user scrolls down the list. To achieve these things the list uses two interfaces: ListModel and ListCellRenderer. List model represents the data; its responsibility is to return the arbitrary object within the list at a given offset. Its second responsibility is to tell the list when the data changes, so the list can refresh.

Think of the model as an array of objects that can tell you when it changes.

The list renderer is like a rubber stamp that knows how to draw an object from the model, it’s called many times per entry in an animated list and must be fast. Unlike standard Codename One components, it’s used to draw the entry in the model and is discarded, hence it has no memory overhead, but if it takes too long to process a model value it can be a big bottleneck!

Think of the render as a translation layer that takes the "data" from the model and translates it to a visual representation.

This is all generic, but a bit too much for most, doing a list correctly requires some understanding. The main source of confusion for developers is the stateless nature of the list and the transfer of state to the model (for example: a checkbox list needs to listen to action events on the list and update the model, in order for the renderer to display that state). Once you understand that it’s easy.

8.17.4. Understanding MVC

A quick recap of what MVC is:

  • Model - Represents the data for the component (list), the model can tell you how many items are in it and which item resides at a given offset within the model. This differs from a simple Vector (or array), since all access to the model is controlled (the interface is simpler), and unlike a Vector/Array, the model can tell you of changes that occur within it.

  • View - The view draws the content of the model. It’s a "dumb" layer that has no notion of what’s displayed and knows how to draw. It tracks changes in the model (the model sends events) and redraws itself when it changes.

  • Controller - The controller accepts user input and performs changes to the model, which in turn cause the view to refresh.

Image by RegisFrey - Own work Public Domain https://commons.wikimedia.org/w/index.php?curid=10298177
Figure 89. Typical MVC Diagram [2]

Codename One’s List component uses the MVC paradigm in its implementation. List itself is the Controller (with a bit of the View mixed in). The ListCellRenderer interface is the rest of the View and the ListModel is (you guessed it by now) the Model.

When the list is painted, it iterates over the visible elements in the model and asks the model for the data, it then draws them using the renderer. Notice that because of this both the model and the renderer must be fast and that’s hard.

Why is this useful

Since the model is a lightweight interface, it can be implemented by you and replaced in runtime if so desired, this allows many use cases:

  1. A list can contain thousands of entries but load the portion visible to the user. Since the model will be queried for the elements that are visible to the user, it won’t need to load the large data set into memory until the user starts scrolling down (at which point other elements may be offloaded from memory).

  2. A list can cache efficiently. For example: a list can mirror data from the server into local RAM without actually downloading all the data. Data can also be mirrored from storage for better performance and discarded for better memory use.

  3. The is no need for state copying. Since renderers allow you to display any object type, the list model interface can be implemented by the application’s data structures (for example: persistence/network engine), which would return internal application data structures saving you the need of copying application state into a list specific data structure. Note that this advantage applies with a custom renderer which is pretty difficult to get right.

  4. Using the proxy pattern you can layer logic such as filtering, sorting, caching, etc. On top of existing models without changing the model source code.

  5. You can reuse generic models for many views, for example: a model that fetches data from the server can be initialized with different arguments, to fetch different data for different views. View objects in different Forms can display the same model instance in different view instances, thus they would update automatically when you change one global model.

Most of these use cases work best for lists that grow to a larger size, or represent complex data, which is what the list object is designed to do.

8.17.5. Important - lists and Layout managers

When working with lists, you want the list to handle the scrolling (otherwise it will perform badly). This means you should place the list in a non-scrollable container (no parent can be scrollable), notice that the content pane is scrollable by default, so you should disable that.

It’s also recommended to place the list in the CENTER location of a BorderLayout to produce the most effective results. For example:

form.setScrollable(false);
form.setLayout(new BorderLayout());
form.add(BorderLayout.CENTER, myList);

8.17.6. MultiList & DefaultListModel

After this long start, here is the first sample of creating a list using the MultiList.

The MultiList is a preconfigured list that contains a ready made renderer with defaults that make sense for the most common use cases. It still retains most of the power available to the List component but reduces the complexity of one of the hardest things to grasp for most developers: rendering.

The full power of the ListModel is still available and allows you to create a million entry list with a few lines of code. For example the objects that the model returns should always be in the form of Map objects and not an arbitrary object like the standard List allows.

Here is a simple example of a MultiList containing a highly popular subject matter:

Form hi = new Form("MultiList", new BorderLayout());

ArrayList<Map<String, Object>> data = new ArrayList<>();

data.add(createListEntry("A Game of Thrones", "1996"));
data.add(createListEntry("A Clash Of Kings", "1998"));
data.add(createListEntry("A Storm Of Swords", "2000"));
data.add(createListEntry("A Feast For Crows", "2005"));
data.add(createListEntry("A Dance With Dragons", "2011"));
data.add(createListEntry("The Winds of Winter", "2016 (please, please, please)"));
data.add(createListEntry("A Dream of Spring", "Ugh"));

DefaultListModel<Map<String, Object>> model = new DefaultListModel<>(data);
MultiList ml = new MultiList(model);
hi.add(BorderLayout.CENTER, ml);
hi.show();
Basic usage of the MultiList and DefaultListModel
Figure 90. Basic usage of the MultiList & DefaultListModel]

createListEntry is trivial:

private Map<String, Object> createListEntry(String name, String date) {
    Map<String, Object> entry = new HashMap<>();
    entry.put("Line1", name);
    entry.put("Line2", date);
    return entry;
}

One major piece is missing here: the cover images for the books. A simple approach would be to place the image objects into the entries using the "icon" property as such:

private Map<String, Object> createListEntry(String name, String date, Image cover) {
    Map<String, Object> entry = new HashMap<>();
    entry.put("Line1", name);
    entry.put("Line2", date);
    entry.put("icon", cover);
    return entry;
}
With cover images in place
Figure 91. With cover images in place
Since the MultiList uses the GenericListCellRenderer internally you can use URLImage to dynamically fetch the data. This is discussed in the graphics section of this guide.
Going further with the ListModel

Suppose that GRRM was prolific and wrote 1 million books. The default list model won’t make much sense in that case but you would still be able to render everything in a list model.

You will fake it a bit but notice that 1M components won’t be created even if you somehow scroll all the way down…​

The ListModel interface can be implemented by anyone in this case you did a stupid simple implementation:

static class GRMMModel implements ListModel<Map<String, Object>> {
    private int selection;
    private final java.util.List<SelectionListener> selectionListeners = new ArrayList<>();

    @Override
    public Map<String, Object> getItemAt(int index) {
        int idx = index % 7;
        switch (idx) {
            case 0:
                return createListEntry("A Game of Thrones " + index, "1996");
            case 1:
                return createListEntry("A Clash Of Kings " + index, "1998");
            case 2:
                return createListEntry("A Storm Of Swords " + index, "2000");
            case 3:
                return createListEntry("A Feast For Crows " + index, "2005");
            case 4:
                return createListEntry("A Dance With Dragons " + index, "2011");
            case 5:
                return createListEntry("The Winds of Winter " + index, "2016 (please, please, please)");
            default:
                return createListEntry("A Dream of Spring " + index, "Ugh");
        }
    }

    @Override
    public int getSize() {
        return 1000000;
    }

    @Override
    public int getSelectedIndex() {
        return selection;
    }

    @Override
    public void setSelectedIndex(int index) {
        int old = selection;
        selection = index;
        for (SelectionListener l : selectionListeners) {
            l.selectionChanged(old, index);
        }
    }

    @Override
    public void addDataChangedListener(DataChangedListener l) {
    }

    @Override
    public void removeDataChangedListener(DataChangedListener l) {
    }

    @Override
    public void addSelectionListener(SelectionListener l) {
        selectionListeners.add(l);
    }

    @Override
    public void removeSelectionListener(SelectionListener l) {
        selectionListeners.remove(l);
    }

    @Override
    public void addItem(Map<String, Object> item) {
    }

    @Override
    public void removeItem(int index) {
    }
}

You can now replace the existing model by removing all the model related logic and changing the constructor call as such:

MultiList ml = new MultiList(new GRMMModel());
Form hi = new Form("Million Entries", new BorderLayout());
hi.add(BorderLayout.CENTER, ml);
hi.show();
It took ages to scroll this far…​ This goes to a million…​
Figure 92. It took ages to scroll this far…​ This goes to a million…​

8.17.7. List cell renderer

The Renderer is a simple interface with 2 methods:

public interface ListCellRenderer {
   //This method is called by the List for each item, when the List paints itself.
   public Component getListCellRendererComponent(List list, Object value, int index, boolean isSelected);

   //This method returns the List animated focus which is animated when list selection changes
   public Component getListFocusComponent(List list);
}

The most simple/naive implementation may choose to implement the renderer as follows:

public Component getListCellRendererComponent(List list, Object value, int index, boolean isSelected){
     return new Label(value.toString());
}

public Component getListFocusComponent(List list){
     return null;
}

This will compile and work, but won’t give you much, notice that you won’t see the List selection move on the List, this is because the renderer returns a Label with the same style regardless if it’s selected or not.

Now make it a bit more useful:

public Component getListCellRendererComponent(List list, Object value, int index, boolean isSelected){
      Label l = new Label(value.toString());
if (isSelected) {
             l.setFocus(true);
             l.getAllStyles().setBgTransparency(100);
         } else {
             l.setFocus(false);
             l.getAllStyles().setBgTransparency(0);
        }
        return l;
}   public Component getListFocusComponent(List list){
      return null;
}

In this renderer you set the Label.setFocus(true) if it’s selected, calling to this method doesn’t give the focus to the Label, it renders the label as selected.

Then you invoke Label.getAllStyles().setBgTransparency(100) to give the selection semi transparency, and 0 for full transparency if not selected.

That’s still not efficient because you create a new Label each time the method is invoked.

To make the code tighter, keep a reference to the Component or extend it as DefaultListCellRenderer does:

class MyRenderer extends Label implements ListCellRenderer {
    public Component getListCellRendererComponent(List list, Object value, int index, boolean isSelected){
        setText(value.toString());
        if (isSelected) {
            setFocus(true);
            getAllStyles().setBgTransparency(100);
        } else {
            setFocus(false);
            getAllStyles().setBgTransparency(0);
        }
        return this;
    }

    public Component getListFocusComponent(List list) {
        // this renderer highlights the selected row itself, and returning the
        // rubber stamp would draw the focus overlay with the last row's text
        return null;
    }
}

Now look at a more advanced Renderer:

class ContactsRenderer extends Container implements ListCellRenderer {

 private Label name = new Label("");
 private Label email = new Label("");
 private Label pic = new Label("");

 private Label focus = new Label("");

 public ContactsRenderer() {
     setLayout(new BorderLayout());
     addComponent(BorderLayout.WEST, pic);
     Container cnt = new Container(new BoxLayout(BoxLayout.Y_AXIS));
     name.getAllStyles().setBgTransparency(0);
     name.getAllStyles().setFont(Font.createTrueTypeFont("native:MainBold"));
     email.getAllStyles().setBgTransparency(0);
     cnt.addComponent(name);
     cnt.addComponent(email);
     addComponent(BorderLayout.CENTER, cnt);

     focus.getStyle().setBgTransparency(100);
 }

 public Component getListCellRendererComponent(List list, Object value, int index, boolean isSelected) {

     Contact person = (Contact) value;
     name.setText(person.getDisplayName());
     email.setText(person.getPrimaryEmail());
     pic.setIcon(person.getPhoto());
     return this;
 }

 public Component getListFocusComponent(List list) {
     return focus;
 }
}

In this renderer you want to render a Contact object to the Screen, you build the Component in the constructor and in the getListCellRendererComponent you update the Labels' texts according to the Contact object.

Notice that in this renderer you return a focus Label with semi transparency, as mentioned before, the focus component can be modified within this method.

For example, you can change the focus Component to have an icon:

focus.getAllStyles().setBgTransparency(100);
focus.setIcon(FontImage.createMaterial(FontImage.MATERIAL_STAR, "Label", 3.0f));
focus.setAlignment(Component.RIGHT);

8.17.8. Generic list cell renderer

As part of the GUI builder work, you needed a way to customize rendering for a List, but the renderer/model approach seemed impossible to adapt to a GUI builder (it seems the Swing GUI builders had a similar issue). Your solution was to introduce the GenericListCellRenderer, which while introducing limitations and implementation requirements still manages to make life easier, both in the GUI builder and outside of it.

GenericListCellRenderer is a renderer designed to be as simple to use as a Component-Container hierarchy, you effectively crammed most of the common renderer use cases into one class. To enable that, you need to know the content of the objects within the model, so the GenericListCellRenderer assumes the model contains Map objects. Since Maps can contain arbitrary data the list model is still generic and allows storing application specific data. Furthermore a Map can still be derived and extended to provide domain specific business logic.

The GenericListCellRenderer accepts two container instances (more later on why at least two, and not one), which it maps to individual Map entries within the model, by finding the appropriate components within the given container hierarchy. Components are mapped to the Map entries based on the name property of the component (getName/setName) and the key/value within the Map, for example:

For a model that contains a Map entry like this:

"Foo": "Bar"
"X": "Y"
"Not": "Applicable"
"Number": Integer(1)

A renderer will loop over the component hierarchy in the container, searching for components whose name matches Foo, X, Not, and Number, and assigning the appropriate value to them.

You can also use image objects as values, and they will be assigned to labels as expected. For example, you can’t assign both an image and a text to a single label, since the key will be taken. That isn’t a big problem, since two labels can be used in such a renderer.

To make matters even more attractive the renderer seamlessly supports list tickering when appropriate, and if a CheckBox appears within the renderer, it will toggle a boolean flag within the Map seamlessly.

One issue that crops up with this approach is that, if a value is missing from the Map, it’s treated as empty and the component is reset.

This can pose an issue if you hardcode an image or text within the renderer and you don’t want them replaced (for example: an arrow graphic on a Label within the renderer). The solution for this is to name the component with Fixed in the end of the name, for example: HardcodedIconFixed.

Naming a component within the renderer with $number will automatically set it as a counter component for the offset of the component within the list.

Styling the GenericListCellRenderer is slightly different, the renderer uses the UIID of the Container passed to the generic list cell renderer, and the background focus uses that same UIID with the word "Focus" appended to it.

Note that the generic list cell renderer will grant focus to the child components of the selected entry if they’re focusable, thus changing the style of said entries. For example: a Container might have a child Label that has one style when the parent container is unselected and another when it’s selected (focused), this can be achieved by defining the label as focusable. Notice that the component will never receive direct focus, since it’s still part of a renderer.

Finally, the generic list cell renderer accepts two or four instances of a Container, rather than the obvious choice of accepting one instance. This allows the renderer to treat the selected entry differently, which is important to tickering, although it’s also useful for the fisheye effect [3]. Since it might not be practical to seamlessly clone the Container for the renderer’s needs, Codename One expects the developer to provide two separate instances, they can be identical in all respects, but they must be separate instances for tickering to work. The renderer also allows for a fisheye effect, where the selected entry is actually different from the unselected entry in its structure, it also allows for a pinstripe effect, where odd/even rows have different styles (this is accomplished by providing 4 instances of the containers selected/unselected for odd/even).

The best way to learn about the generic list cell renderer and the Map model is by playing with them in the old GUI builder. Notice they can be used in code without any dependency on the GUI builder and can be useful at that.

Here is a simple example of a list with checkboxes that gets updated automatically:

list.setRenderer(new GenericListCellRenderer(createGenericRendererContainer(), createGenericRendererContainer()));
GenericListCellRenderer demo code
Figure 93. GenericListCellRenderer demo code
Custom UIID of entry in GenericListCellRenderer/MultiList

With MultiList/GenericListCellRenderer one of the common issues is making a UI where a specific component within the list renderer has a different UIID style based on data. For example: this can be helpful to mark a label within the list as red, for instance, for a list of monetary transactions.

This can be achieved with a custom renderer, but that’s a pretty difficult task.
GenericListCellRenderer (MultiList uses GenericListCellRenderer internally) has another option.

Normally, to build the model for a renderer of this type, you use something like:

map.put("componentName", "Component Value");

What if you want componentName to be red? Just use:

map.put("componentName_uiid", "red");

This will apply the UIID "red" to the component, which you can then style in the theme. Notice that once you start doing this, you need to define this entry for all entries, for example:

map.put("componentName_uiid", "blue");

Otherwise the component will stay red for the next entry (since the renderer acts like a rubber stamp).

Rendering prototype

Because of the rendering architecture of a List its pretty hard to calculate the right preferred size for such a component. The default behavior includes querying a few entries from the model then constructing their renderers to get a "sample" of the preferred size value.

As you might guess this triggers a performance penalty that’s paid with every reflow of the UI. The solution is to use setRenderingPrototype.

setRenderingPrototype accepts a "fake" value that represents a reasonably large amount of data and it will be used to calculate the preferred size. For example: for a multiList that should render 2 lines of text with 20 characters and a 5mm square icon you can do something like this:

Map<String, Object> proto = new HashMap<>();
proto.put("Line1", "WWWWWWWWWWWWWWWWWWWW");
proto.put("Line2", "WWWWWWWWWWWWWWWWWWWW");
int mm5 = Display.getInstance().convertToPixels(5, true);
proto.put("icon", Image.createImage(mm5, mm5));
myMultiList.setRenderingPrototype(proto);

8.17.9. ComboBox

The ComboBox is a specialization of List that displays a single selected entry. When clicking that entry a popup is presented allowing the user to pick an entry from the full list of entries.

The ComboBox UI paradigm isn’t as common on OSes such as iOS where there is no native equivalent to it. Recommend using either the Picker class or the AutoCompleteTextField.

ComboBox is notoriously hard to style as it relies on a complex dynamic of popup renderer and instantly visible renderer. The UIID for the ComboBox is ComboBox but if you set it to something else all the other UIID’s will also change their prefix. For example: the ComboBoxPopup UIID will become MyNewUIIDPopup.

The combo box defines the following UIID’s by default:

  • ComboBox

  • ComboBoxItem

  • ComboBoxFocus

  • PopupContentPane

  • PopupItem

  • PopupFocus

The ComboBox also defines theme constants that allow some native themes to manipulate its behavior for example:

  • popupTitleBool - shows the "label for" value as the title of the popup dialog

  • popupCancelBodyBool - Adds a cancel button into the popup dialog

  • centeredPopupBool - shows the popup dialog in the center of the screen instead of under the popup

  • otherPopupRendererBool - Uses a different list cell render for the popup than the one used for the ComboBox itself. When this is false PopupItem & PopupFocus become irrelevant. Notice that the Android native theme defines this to true.

Since a ComboBox is a List you can use everything you learned about a List to build a ComboBox including models, GenericListCellRenderer etc.

For example: the demo below uses the GRRM demo data from above to build a ComboBox:

Form hi = new Form("ComboBox", new BoxLayout(BoxLayout.Y_AXIS));
ComboBox<Map<String, Object>> combo = new ComboBox<> (
        createListEntry("A Game of Thrones", "1996"),
        createListEntry("A Clash Of Kings", "1998"),
        createListEntry("A Storm Of Swords", "2000"),
        createListEntry("A Feast For Crows", "2005"),
        createListEntry("A Dance With Dragons", "2011"),
        createListEntry("The Winds of Winter", "2016 (please, please, please)"),
        createListEntry("A Dream of Spring", "Ugh"));

combo.setRenderer(new GenericListCellRenderer<>(new MultiButton(), new MultiButton()));
hi.add(combo);
hi.show();
GRRM ComboBox
Figure 94. GRRM ComboBox

8.18. Slider

A Slider is an empty component that can be filled horizontally or vertically to allow indicating progress, setting volume etc. It can be editable to allow the user to determine its value or none editable to relay that information to the user. It can have a thumb on top to show its current position.

Slider
Figure 95. Slider

The interesting part about the slider is that it has two separate style UIID’s, Slider & SliderFull. The Slider UIID is always painted and SliderFull is rendered on top based on the amount the Slider should be filled.

Slider is highly customizable for example: a slider can be used to replicate a 5-star rating widget as such. Notice that this slider will work when its given its preferred size otherwise more stars will appear. That’s why you place it within a FlowLayout:

Form hi = new Form("Star Slider", new BoxLayout(BoxLayout.Y_AXIS));
hi.add(FlowLayout.encloseCenter(createStarRankSlider()));
hi.show();

The slider itself is initialized in the code below. Notice that you can achieve almost the same result using a theme by setting the Slider & SliderFull UIID’s (both in selected & unselected states).

In fact doing this in the theme might be superior as you could use one image that contains 5 stars already and that way you won’t need the preferred size hack below:

private void initStarRankStyle(Style s, Image star) {
    s.setBackgroundType(Style.BACKGROUND_IMAGE_TILE_BOTH);
    s.setBorder(Border.createEmpty());
    s.setBgImage(star);
    s.setBgTransparency(0);
}

private Slider createStarRankSlider() {
    Slider starRank = new Slider();
    starRank.setEditable(true);
    starRank.setMinValue(0);
    starRank.setMaxValue(10);
    Font fnt = Font.createTrueTypeFont("native:MainLight", "native:MainLight").
            derive(Display.getInstance().convertToPixels(5, true), Font.STYLE_PLAIN);
    Style s = new Style(0xffff33, 0, fnt, (byte)0);
    Image fullStar = FontImage.createMaterial(FontImage.MATERIAL_STAR, s).toImage();
    s.setOpacity(100);
    s.setFgColor(0);
    Image emptyStar = FontImage.createMaterial(FontImage.MATERIAL_STAR, s).toImage();
    initStarRankStyle(starRank.getSliderEmptySelectedStyle(), emptyStar);
    initStarRankStyle(starRank.getSliderEmptyUnselectedStyle(), emptyStar);
    initStarRankStyle(starRank.getSliderFullSelectedStyle(), fullStar);
    initStarRankStyle(starRank.getSliderFullUnselectedStyle(), fullStar);
    starRank.setPreferredSize(new Dimension(fullStar.getWidth() * 5, fullStar.getHeight()));
    return starRank;
}
private void showStarPickingForm() {
  Form hi = new Form("Star Slider", new BoxLayout(BoxLayout.Y_AXIS));
  hi.add(FlowLayout.encloseCenter(createStarRankSlider()));
  hi.show();
}
Star Slider set to 5 (its between 0 - 10)
Figure 96. Star Slider set to 5 (its between 0 - 10)
This slider goes all the way to 0 stars which is less common. You can use a Label to represent the first star and have the slider work between 0 - 8 values to provide 4 more stars.

8.19. Table

Table is a composite component (but it isn’t a lead component), this means it’s a subclass of Container. It’s effectively built from many components.

Table is based on the TableLayout class. It’s important to be familiar with that layout manager when working with Table.

Here is a trivial sample of using the standard table component:

Form hi = new Form("Table", new BorderLayout());
TableModel model = new DefaultTableModel(
new String[] {"Col 1", "Col 2", "Col 3"},
new Object[][] {
{"Row 1", "Row A", "Row X"},
{"Row 2", "Row B", "Row Y"},
{"Row 3", "Row C", "Row Z"},
{"Row 4", "Row D", "Row K"},
}) {
public boolean isCellEditable(int row, int col) {
return col != 0;
}
};
Table table = new Table(model);
hi.add(BorderLayout.CENTER, table);
hi.show();
Simple Table usage
Figure 97. Simple Table usage
In the sample above the title area and first column aren’t editable. The other two columns are editable.

The more "interesting" capabilities of the Table class can be utilized through the TableLayout. You can use the layout constraints (also exposed in the table class) to create spanning and elaborate UI’s.

For example:

Form hi = new Form("Table", new BorderLayout());
TableModel model = new DefaultTableModel(
new String[] {"Col 1", "Col 2", "Col 3"},
new Object[][] {
{"Row 1", "Row A", "Row X"},
{"Row 2", "Row B can now stretch", null},
{"Row 3", "Row C", "Row Z"},
{"Row 4", "Row D", "Row K"},
}) {
public boolean isCellEditable(int row, int col) {
return col != 0;
}
};
Table table = new Table(model) {
@Override
protected TableLayout.Constraint createCellConstraint(Object value, int row, int column) {
TableLayout.Constraint con = super.createCellConstraint(value, row, column);
if (row == 1 && column == 1) {
con.setHorizontalSpan(2);
}
con.setWidthPercentage(33);
return con;
}
};
hi.add(BorderLayout.CENTER, table);
hi.show();
Table with spanning and fixed widths to 33%
Figure 98. Table with spanning & fixed widths to 33%

To customize the table cell behavior you can derive the Table to create a "renderer like" widget, but unlike the list this component is "kept" and used as is. This means you can bind listeners to this component and work with it as you would with any other component in Codename One.

The example above can be extended to include far more capabilities:

Form hi = new Form("Table", new BorderLayout());
TableModel model = new DefaultTableModel(
new String[] {"Col 1", "Col 2", "Col 3"},
new Object[][] {
{"Row 1", "Row A", "Row X"},
{"Row 2", "Row B can now stretch", null},
{"Row 3", "Row C", "Row Z"},
{"Row 4", "Row D", "Row K"},
}) {
public boolean isCellEditable(int row, int col) {
return col != 0;
}
};
Table table = new Table(model) {
@Override
protected Component createCell(Object value, int row, int column, boolean editable) {
Component cell;
if (row == 1 && column == 1) {
Picker p = new Picker();
p.setType(Display.PICKER_TYPE_STRINGS);
p.setStrings("Row B can now stretch", "This is a good value",
"So Is This", "Better than text field");
p.setSelectedString((String)value);
p.setUIID("TableCell");
p.addActionListener(e -> getModel().setValueAt(row, column, p.getSelectedString()));
cell = p;
} else {
cell = super.createCell(value, row, column, editable);
}
if (row > -1 && row % 2 == 0) {
cell.getAllStyles().setBgColor(0xeeeeee);
cell.getAllStyles().setBgTransparency(255);
}
return cell;
}

@Override
protected TableLayout.Constraint createCellConstraint(Object value, int row, int column) {
TableLayout.Constraint con = super.createCellConstraint(value, row, column);
if (row == 1 && column == 1) {
con.setHorizontalSpan(2);
}
con.setWidthPercentage(33);
return con;
}
};
hi.add(BorderLayout.CENTER, table);
hi.show();
Table with customize cells using the pinstripe effect
Figure 99. Table with customize cells using the pinstripe effect
Picker table cell during edit
Figure 100. Picker table cell during edit

To line wrap table cells you can override the createCell method and return a TextArea instead of a TextField since the TextArea defaults to the multi-line behavior this should work seamlessly. For example:

Form hi = new Form("Table", new BorderLayout());
TableModel model = new DefaultTableModel(
        new String[] {"Name", "Description", "Status"},
        new Object[][] {
            {"Row 1", "A short value", "OK"},
            {"Row 2", "A much longer value that wraps to multiple lines", "OK"},
            {"Row 3", "Another long table cell that needs wrapping", "Pending"},
        });
Table table = new Table(model) {
    @Override
    protected Component createCell(Object value, int row, int column, boolean editable) {
        if (row > -1 && column == 1) {
            TextArea cell = new TextArea(String.valueOf(value));
            cell.setUIID("TableCell");
            cell.setEditable(false);
            cell.setGrowByContent(true);
            return cell;
        }
        return super.createCell(value, row, column, editable);
    }

    @Override
    protected TableLayout.Constraint createCellConstraint(Object value, int row, int column) {
        TableLayout.Constraint con = super.createCellConstraint(value, row, column);
        con.setWidthPercentage(column == 1 ? 50 : 25);
        return con;
    }
};
hi.add(BorderLayout.CENTER, table);
hi.show();
Notice that you don’t need to do anything else as binding to the TextArea is built-in to the Table.
You must set the column width constraint when you want multi-line to work. Otherwise the preferred size of the column might be too wide and the remaining columns might not have space left.
Multiline table cell in portrait mode
Figure 101. Multiline table cell in portrait mode
Multiline table cell in landscape mode. Notice the cell row count adapts seamlessly
Figure 102. Multiline table cell in landscape mode. Notice the cell row count adapts seamlessly

8.19.1. Sorting tables

Sorting tables by clicking the titles is something that should work out of the box by using an API like setSortSupported(true):

Form hi = new Form("Table", new BorderLayout());
TableModel model = new DefaultTableModel(
        new String[] {"Col 1", "Col 2", "Col 3"},
        new Object[][] {
            {"Row 1", "Row A", 1},
            {"Row 2", "Row B", 4},
            {"Row 3", "Row C", 7.5},
            {"Row 4", "Row D", 2.24},
        });
Table table = new Table(model);
table.setSortSupported(true);
hi.add(BorderLayout.CENTER, table);
hi.add(BorderLayout.NORTH, new Button("Button"));
hi.show();

Notice this works with numbers, Strings and might work with dates but you can support any object type by overriding the method protected Comparator createColumnSortComparator(int column) which should return a comparator for your custom object type in the column.

8.20. Tree

Tree allows displaying hierarchical data such as folders and files in a collapsible/expandable UI. Like the Table it’s a composite component (but it isn’t a lead component). Like the Table it works in consort with a model to construct its user interface on the fly but doesn’t use a stateless renderer (as List does).

The data of the Tree arrives from a model for example: this:

class StringArrayTreeModel implements TreeModel {
    String[][] arr = new String[][] {
        {"Colors", "Letters", "Numbers"},
        {"Red", "Green", "Blue"},
        {"A", "B", "C"},
        {"1", "2", "3"}
    };

    @Override
    public Vector getChildren(Object parent) {
        Vector v = new Vector();
        if (parent == null) {
            for (int iter = 0; iter < arr[0].length; iter++) {
                v.addElement(arr[0][iter]);
            }
            return v;
        }
        for (int iter = 0; iter < arr[0].length; iter++) {
            if (parent.equals(arr[0][iter]) && arr.length > iter + 1 && arr[iter + 1] != null) {
                for (int i = 0; i < arr[iter + 1].length; i++) {
                    v.addElement(arr[iter + 1][i]);
                }
            }
        }
        return v;
    }

    @Override
    public boolean isLeaf(Object node) {
        Vector v = getChildren(node);
        return v == null || v.size() == 0;
    }
}

Tree dt = new Tree(new StringArrayTreeModel());
Form hi = new Form("Tree", new BorderLayout());
hi.add(BorderLayout.CENTER, dt);
hi.show();

Will result in this:

Tree
Figure 103. Tree
Since Tree is hierarchy based you can’t have a simple model like you’ve for the Table as deep hierarchy is harder to represent with arrays.

A more practical "real world" example would be working with XML data. You can use something like this to show an XML Tree:

Form hi = new Form("XML Tree", new BorderLayout());
// Parsed inline so the sample runs as it stands; in an application this
// would come from the network or from a packaged resource.
String xml = "<project name=\"demo\">"
        + "<target name=\"compile\"><javac srcdir=\"src\"/></target>"
        + "<target name=\"jar\"><zip destfile=\"demo.jar\"/></target>"
        + "</project>";
try (Reader r = new CharArrayReader(xml.toCharArray())) {
    Element e = new XMLParser().parse(r);
    Tree tree = new Tree(new XMLTreeModel(e)) {
        @Override
        protected String childToDisplayLabel(Object child) {
            if (child instanceof Element) {
                Element el = (Element) child;
                // getTagName() throws for the elements that carry text
                if (el.isTextElement()) {
                    return el.getText();
                }
                return el.getTagName();
            }
            return child.toString();
        }
    };
    hi.add(BorderLayout.CENTER, tree);
    xmlTree = tree;
} catch (IOException err) {
    Log.e(err);
}
hi.show();
The try with resources syntax closes the Reader for you however the block ends.
XML Tree
Figure 104. XML Tree

The model for the XML hierarchy is implemented as such:

/// Walks a parsed XML document as a tree: the document element is the single
/// root, and a node's children are the element's children.
static class XMLTreeModel implements TreeModel {
    private Element root;

    XMLTreeModel(Element e) {
        root = e;
    }

    @Override
    public Vector getChildren(Object parent) {
        if (parent == null) {
            Vector c = new Vector();
            c.addElement(root);
            return c;
        }
        Vector result = new Vector();
        Element e = (Element) parent;
        for (int iter = 0; iter < e.getNumChildren(); iter++) {
            result.addElement(e.getChildAt(iter));
        }
        return result;
    }

    @Override
    public boolean isLeaf(Object node) {
        Element e = (Element) node;
        return e.getNumChildren() == 0;
    }
}

8.21. ShareButton

ShareButton is a button you can add into the UI to let a user share an image or block of text.

The ShareButton uses a set of predefined share options on the simulator. On Android & iOS the ShareButton is mapped to the OS native sharing functionality and can share the image/text with the services configured on the device (for example: Twitter, Facebook etc.).

Sharing text is trivial but to share an image you need to save it to the FileSystemStorage. Notice that saving to Storage won’t work!

In the sample code below you take a screenshot which is saved to FileSystemStorage for sharing:

Form hi = new Form("ShareButton");
ShareButton sb = new ShareButton();
sb.setText("Share Screenshot");
hi.add(sb);

Image screenshot = Image.createImage(hi.getWidth(), hi.getHeight());
hi.revalidate();
hi.setVisible(true);
hi.paintComponent(screenshot.getGraphics(), true);

String imageFile = FileSystemStorage.getInstance().getAppHomePath() + "screenshot.png";
try(OutputStream os = FileSystemStorage.getInstance().openOutputStream(imageFile);) {
    ImageIO.getImageIO().save(screenshot, os, ImageIO.FORMAT_PNG, 1);
} catch(IOException err) {
    Log.e(err);
}
sb.setImageToShare(imageFile, "image/png");
The share button running on the simulator
Figure 105. The share button running on the simulator
ShareButton behaves differently on the device…​
The share button running on the Android device and screenshot sent into twitter
Figure 106. The share button running on the Android device and screenshot sent into twitter
The ShareButton features some share service classes to allow plugging in more share services. For example, this functionality is relevant to devices where native sharing isn’t supported. This code isn’t used on iOS/Android…​

8.21.1. Share Result Callback

The share API can report what the user did with the share sheet. Register a ShareResultListener on either ShareButton or directly with Display.share(…​) and you receive a single ShareResult describing the outcome:

  • SHARED_TO(packageName) — the user picked a destination. packageName is the chosen target’s identifier: an Android package name (for example, com.whatsapp) or, on iOS, a UIActivityType such as com.apple.UIKit.activity.PostToTwitter. It may be null when the platform doesn’t expose the selection (for example, older Android, Web Share API).

  • DISMISSED — the user cancelled without picking a target. iOS reports this reliably; Android’s chooser has no public dismissal signal, so on Android the listener simply doesn’t fire on cancel.

  • FAILED — the share couldn’t be completed. getError() may carry a short platform-supplied message.

The listener is always invoked on the EDT, exactly once per share request.

ShareButton sb = new ShareButton();
sb.setTextToShare("Check this out!");
sb.setShareResultListener(result -> {
    if (result.isSharedTo()) {
        Log.p("Shared to " + result.getPackageName());
    } else if (result.isDismissed()) {
        Log.p("User dismissed the share sheet");
    } else if (result.isFailed()) {
        Log.p("Share failed: " + result.getError());
    }
});
form.add(sb);

If you call Display directly instead of using ShareButton, pass the listener as the final argument to the new overload:

Display.getInstance().share(
        "Check this out!", imagePath, "image/png", sourceRect,
        result -> handleResult(result));

When the share is dispatched through the non-native fallback dialog (platforms without isNativeShareSupported), the listener still fires: it reports DISMISSED when the user picks the Cancel button, and the underlying ShareService implementations report SHARED_TO once they call ShareService.finish(). Custom ShareService subclasses can call deliverResult(ShareResult.failed("…​")) from inside share(…​) to publish a failure explicitly; otherwise finish() falls back to a default SHARED_TO(commandName).

8.22. iOS Share Extension Authoring Helper

An iOS share extension is a separate app target that the system shows inside UIActivityViewController when the user shares from another app (Safari, Photos, etc.). It runs in a sandboxed process and persists payloads to the host app via an App Group shared NSUserDefaults suite.

Codename One’s build pipeline already wires .ios.appext zip archives under src/main/resources into the generated Xcode project. The IOSShareExtensionBuilder helper generates that archive for you so you don’t have to create the extension target in Xcode first.

8.22.1. What the helper produces

For a single call the helper writes four files at the root of the extension bundle:

  • Info.plist — declares the NSExtension dictionary, activation rules (SupportsText / SupportsWebURL / SupportsImage), and the principal class (ShareViewController).

  • <ExtensionName>.entitlements — declares com.apple.security.application-groups with the configured App Group identifier.

  • ShareViewController.swift — a SLComposeServiceViewController subclass that extracts text / URL / image attachments from extensionContext, packs them into a dictionary, and writes that dictionary to UserDefaults(suiteName: appGroupId) under the key cn1.shareExtension.payload.

  • buildSettings.properties — Xcode build setting overrides (deployment target, Swift version, entitlements path, plist path) picked up by the iOS build pipeline.

8.22.2. Generating the bundle

Call the builder from a Maven plugin, an Ant task or a one-shot main:

IOSShareExtensionBuilder lives in the Codename One Maven plugin rather than in the framework, so this runs at build time and isn’t one of the compiled examples:

new IOSShareExtensionBuilder()
        .setExtensionName("MyShareExtension")
        .setDisplayName("Share to MyApp")
        .setHostBundleId("com.example.myapp")
        .setAppGroupId("group.com.example.myapp")
        .acceptText(true)
        .acceptURLs(true)
        .acceptImages(true)
        .writeAppext(new File("src/main/resources/MyShareExtension.ios.appext"));

The next iOS build picks up the .ios.appext archive automatically; no Xcode steps are required.

If you prefer a directory layout (for example, to commit the generated sources or to feed a future ios/app_extensions/ pipeline) use writeTo(File) instead of writeAppext(File) — the file contents are identical.

8.22.3. Reading the payload from the host app

The host app has to be in that App Group too. setAppGroupId writes the entitlement for the extension only, so add the same identifier to the application with the ios.app_groups build hint — the two sides are reading and writing one container, and a mismatch produces an empty dictionary rather than an error:

codename1.arg.ios.app_groups=group.com.example.myapp

It then reads the most recent payload at startup or when it resumes:

// Inside an iOS native interface
NSUserDefaults* shared =
    [[NSUserDefaults alloc] initWithSuiteName:@"group.com.example.myapp"];
NSDictionary* payload = [shared dictionaryForKey:@"cn1.shareExtension.payload"];

The payload dictionary contains:

  • text — the user-composed text (String).

  • items — an array of { kind, value } dictionaries where kind is "text", "url" or "image". For images, value is a file URL inside the App Group container.

  • timestamp — a Double UNIX time.

Expose this to your CN1 code via a small native interface and clear the key once handled so the next share is unambiguous.

8.22.4. Activation rules and validation

The builder validates its inputs before writing anything:

  • extensionName must be a non-empty identifier (letters, digits, _, -). It becomes the Xcode target name and the .appex bundle name. The extension’s bundle id is set to <hostBundleId>.<extensionName> by the iOS build pipeline.

  • appGroupId must start with group. (an Apple requirement). Create the matching App Group in your Apple Developer account and add it to both the host app’s and the extension’s provisioning profile.

  • At least one of acceptText, acceptURLs, acceptImages must be enabled, otherwise iOS will never present your extension.

The default deployment target is iOS 12.0; override with setDeploymentTarget("13.0") if you need newer APIs in your customized controller.

8.22.5. Customising the generated controller

The generated Swift controller covers the common case (read attachments, persist to App Group, finish the request). If you need bespoke logic — custom UI, server upload, image transcoding — treat the helper’s output as a starting point: run it once, copy the directory under ios/app_extensions/MyShareExtension/, edit ShareViewController.swift, and zip it back into .ios.appext (or wait for the directory-based pipeline tracked by issue #3427).

Provisioning profiles for the extension target aren’t generated by the helper. You still need to provision the extension’s bundle id (<hostBundleId>.<extensionName>) and add the App Group capability to it on the Apple Developer portal, alongside the host app.

8.23. Tabs

The Tabs Container arranges components into groups within "tabbed" containers. Tabs is a container type that allows leafing through its children using labeled toggle buttons. The tabs can be placed in many different ways (top, bottom, left or right) with the default being determined by the platform. This class also allows swiping between components to leaf between said tabs (for this purpose the tabs themselves can also be hidden).

Since Tabs are a Container its a common mistake to try and add a Tab using the add method. That method won’t work since a Tab can have both an Image and text String associated with it:

Form hi = new Form("Tabs", new BorderLayout());

Tabs t = new Tabs();
Style s = UIManager.getInstance().getComponentStyle("Tab");
FontImage icon1 = FontImage.createMaterial(FontImage.MATERIAL_QUESTION_ANSWER, s);

Container container1 = BoxLayout.encloseY(new Label("Label1"), new Label("Label2"));
t.addTab("Tab1", icon1, container1);
t.addTab("Tab2", new SpanLabel("Some text directly in the tab"));

hi.add(BorderLayout.CENTER, t);
Simple usage of Tabs
Figure 107. Simple usage of Tabs

A common usage for Tabs is the swipe to proceed effect which is common in iOS applications. In the code below you use RadioButton and LayeredLayout with hidden tabs to produce that effect:

Form hi = new Form("Swipe Tabs", new LayeredLayout());
Tabs t = new Tabs();
t.hideTabs();

Style s = UIManager.getInstance().getComponentStyle("Button");
FontImage radioEmptyImage = FontImage.createMaterial(FontImage.MATERIAL_RADIO_BUTTON_UNCHECKED, s);
FontImage radioFullImage = FontImage.createMaterial(FontImage.MATERIAL_RADIO_BUTTON_CHECKED, s);
((DefaultLookAndFeel) UIManager.getInstance().getLookAndFeel())
        .setRadioButtonImages(radioFullImage, radioEmptyImage, radioFullImage, radioEmptyImage);

Container container1 = BoxLayout.encloseY(new Label("Swipe the tab to see more"),
        new Label("You can put anything here"));
t.addTab("Tab1", container1);
t.addTab("Tab2", new SpanLabel("Some text directly in the tab"));

RadioButton firstTab = new RadioButton("");
RadioButton secondTab = new RadioButton("");
firstTab.setUIID("Container");
secondTab.setUIID("Container");
new ButtonGroup(firstTab, secondTab);
firstTab.setSelected(true);
Container tabsFlow = FlowLayout.encloseCenter(firstTab, secondTab);

hi.add(t);
hi.add(BorderLayout.south(tabsFlow));

t.addSelectionListener((i1, i2) -> {
    switch (i2) {
        case 0:
            if (!firstTab.isSelected()) {
                firstTab.setSelected(true);
            }
            break;
        case 1:
            if (!secondTab.isSelected()) {
                secondTab.setSelected(true);
            }
            break;
        default:
            break;
    }
});
Swipeable Tabs with an iOS carousel effect page 1
Figure 108. Swipeable Tabs with an iOS carousel effect page 1
Swipeable Tabs with an iOS carousel effect page 2
Figure 109. Swipeable Tabs with an iOS carousel effect page 2
Notice that you used setRadioButtonImages to explicitly set the radio button images to the look you want for the carousel.

8.23.1. Animated tab indicator

Modern Material 3 (NavigationBar) and iOS 26 tab bars animate a small underline between tabs when the user changes selection. Tabs ships this as an opt-in feature gated by the tabsAnimatedIndicatorBool theme constant; the iOS Modern and Android Material themes turn it on by default, so apps shipping against those themes get the effect for free.

To enable in a custom theme:

#Constants {
    tabsAnimatedIndicatorBool: true;
    tabsAnimatedIndicatorDurationInt: 200;    /* tween duration in ms */
    tabsAnimatedIndicatorThicknessMm: 1;      /* underline thickness  */
}

TabIndicator {
    /* The indicator picks up its color from this UIID's fg. If the
       UIID isn't defined or has fgColor == 0, the indicator falls
       back to the currently-selected tab's fgColor. */
    color: #007aff;
    background-color: transparent;
    padding: 0;
    margin: 0;
}

To toggle programmatically (for example, add it to a Tabs instance whose theme hasn’t enabled it):

Tabs tabs = new Tabs();
tabs.setAnimatedIndicator(true);

The indicator animates its x / width from the previously selected tab’s bounds to the new selection’s bounds using a Motion.createEaseInOutMotion over the configured duration (200ms default, matching Material 3’s spec). Rapid double-taps start the animation from the current interpolated position rather than from a stale baseline, so the indicator chains cleanly.

8.24. MediaManager & MediaPlayer

MediaPlayer is a peer component, understanding this is crucial if your application depends on such a component. You can learn about peer components and their issues here.

The MediaPlayer allows you to control video playback. To use the MediaPlayer you need to first load the Media object from the MediaManager.

The MediaManager is the core class responsible for media interaction in Codename One.

You should also check out the Capture class for things that aren’t covered by the MediaManager.

In the demo code below you use the gallery functionality to pick a video from the device’s video gallery:

final Form hi = new Form("MediaPlayer", new BorderLayout());
hi.setToolbar(new Toolbar());
Style s = UIManager.getInstance().getComponentStyle("Title");
FontImage icon = FontImage.createMaterial(FontImage.MATERIAL_VIDEO_LIBRARY, s);
hi.getToolbar().addCommandToRightBar("", icon, (evt) -> {
    Display.getInstance().openGallery((e) -> {
        if(e != null && e.getSource() != null) {
            String file = (String)e.getSource();
            try {
                Media video = MediaManager.createMedia(file, true);
                hi.removeAll();
                hi.add(BorderLayout.CENTER, new MediaPlayer(video));
                hi.revalidate();
            } catch(IOException err) {
                Log.e(err);
            }
        }
    }, Display.GALLERY_VIDEO);
});
hi.show();
Video playback running on the simulator
Figure 110. Video playback running on the simulator
Video playback running on an Android device. Notice the native playback controls that appear when the video is tapped
Figure 111. Video playback running on an Android device. Notice the native playback controls that appear when the video is tapped
Video playback in the simulator will work with JavaFX enabled. This is the default for Java 8 or newer so recommend using that.

8.25. ImageViewer

The ImageViewer allows you to inspect, zoom and pan into an image. It also allows swiping between images if you have a set of images (using an image list model).

The ImageViewer is a complex rich component designed for user interaction. If you want to display an image use Label if you want the image to scale seamlessly use ScaleImageLabel.

You can use the ImageViewer as a tool to view a single image which allows you to zoom in/out to that image as such:

Image duke = FontImage.createMaterial(FontImage.MATERIAL_INFO, "Label", 3.0f);
Form hi = new Form("ImageViewer", new BorderLayout());
ImageViewer iv = new ImageViewer(duke);
hi.add(BorderLayout.CENTER, iv);
hi.show();
You can simulate pinch to zoom on the simulator by dragging the right button away from the top left corner to zoom in and towards the top left corner to zoom out. On Mac touchpads you can drag two fingers to achieve that.
ImageViewer as the demo loads with the image from the default icon
Figure 112. ImageViewer as the demo loads with the image from the default icon
ImageViewer zoomed in
Figure 113. ImageViewer zoomed in

You can work with a list of images to produce a swiping effect for the image viewer where you can swipe from one image to the next and also zoom in/out on a specific image:

Form hi = new Form("ImageViewer", new BorderLayout());

Image red = Image.createImage(100, 100, 0xffff0000);
Image green = Image.createImage(100, 100, 0xff00ff00);
Image blue = Image.createImage(100, 100, 0xff0000ff);
Image gray = Image.createImage(100, 100, 0xffcccccc);

ImageViewer iv = new ImageViewer(red);
iv.setImageList(new DefaultListModel<>(red, green, blue, gray));
hi.add(BorderLayout.CENTER, iv);
An ImageViewer with many elements is indistinguishable from a single ImageViewer except for swipe
Figure 114. An ImageViewer with many elements is indistinguishable from a single ImageViewer except for swipe

Notice that you use a ListModel to allow swiping between images.

ImageViewer also supports optional side arrows (material font icons) and an optional thumbnail strip for direct image navigation:

Image red = FontImage.createMaterial(FontImage.MATERIAL_LOOKS_ONE, "Label", 6.0f);
Image green = FontImage.createMaterial(FontImage.MATERIAL_LOOKS_TWO, "Label", 6.0f);
Image blue = FontImage.createMaterial(FontImage.MATERIAL_LOOKS_3, "Label", 6.0f);
Image gray = FontImage.createMaterial(FontImage.MATERIAL_LOOKS_4, "Label", 6.0f);
ImageViewer iv = new ImageViewer(red);
iv.setImageList(new DefaultListModel<>(red, green, blue, gray));
iv.setNavigationArrowsVisible(true);
iv.setThumbnailsVisible(true);
iv.setThumbnailBarHeight(6f); // Optional, defaults to 6mm
Form hi = new Form("ImageViewer", new BorderLayout());
hi.add(BorderLayout.CENTER, iv);
hi.show();

When arrows are enabled, tapping near the left/right edge will move to the previous/next image. When thumbnails are enabled, tapping a thumbnail jumps directly to that image.

All three options can be configured with theme constants (using the ImageViewer UIID prefix):

  • imageviewerNavigationArrowsBool=true|false

  • imageviewerThumbnailsBool=true|false

  • imageviewerThumbnailBarHeightMM=<floating point millimeters>

EncodedImage’s aren’t always fully loaded and so when you swipe if the images are large you might see delays!

You can dynamically download images directly into the ImageViewer with a custom list model like this:

Style s = UIManager.getInstance().getComponentStyle("Label");
Form hi = new Form("ImageViewer", new BorderLayout());
final EncodedImage placeholder = EncodedImage.createFromImage(
        FontImage.createMaterial(FontImage.MATERIAL_SYNC, s).
                scaled(300, 300), false);

class ImageList implements ListModel<Image> {
    private int selection;
    private final java.util.List<SelectionListener> selectionListeners = new ArrayList<>();
    private String[] imageURLs = {
        "https://awoiaf.westeros.org/images/thumb/9/93/AGameOfThrones.jpg/300px-AGameOfThrones.jpg",
        "https://awoiaf.westeros.org/images/thumb/3/39/AClashOfKings.jpg/300px-AClashOfKings.jpg",
        "https://awoiaf.westeros.org/images/thumb/2/24/AStormOfSwords.jpg/300px-AStormOfSwords.jpg",
        "https://awoiaf.westeros.org/images/thumb/a/a3/AFeastForCrows.jpg/300px-AFeastForCrows.jpg",
        "https://awoiaf.westeros.org/images/7/79/ADanceWithDragons.jpg"
    };
    private Image[] images;
    private EventDispatcher listeners = new EventDispatcher();

    public ImageList() {
        this.images = new EncodedImage[imageURLs.length];
    }

    public Image getItemAt(final int index) {
        if(images[index] == null) {
            images[index] = placeholder;
            Util.downloadUrlToStorageInBackground(imageURLs[index], "list" + index, (e) -> {
                    try {
                        try(InputStream is = Storage.getInstance().createInputStream("list" + index)) {
                            // EncodedImage.create reads the stream but does not close it
                            images[index] = EncodedImage.create(is);
                        }
                        listeners.fireDataChangeEvent(index, DataChangedListener.CHANGED);
                    } catch(IOException err) {
                        err.printStackTrace();
                    }
            });
        }
        return images[index];
    }

    public int getSize() {
        return imageURLs.length;
    }

    public int getSelectedIndex() {
        return selection;
    }

    public void setSelectedIndex(int index) {
        int old = selection;
        selection = index;
        // ImageViewer swaps the displayed image from this event
        for(SelectionListener l : selectionListeners) {
            l.selectionChanged(old, index);
        }
    }

    public void addDataChangedListener(DataChangedListener l) {
        listeners.addListener(l);
    }

    public void removeDataChangedListener(DataChangedListener l) {
        listeners.removeListener(l);
    }

    public void addSelectionListener(SelectionListener l) {
        selectionListeners.add(l);
    }

    public void removeSelectionListener(SelectionListener l) {
        selectionListeners.remove(l);
    }

    public void addItem(Image item) {
    }

    public void removeItem(int index) {
    }
};

ImageList imodel = new ImageList();

ImageViewer iv = new ImageViewer(imodel.getItemAt(0));
iv.setImageList(imodel);
hi.add(BorderLayout.CENTER, iv);
hi.show();
Dynamically fetching an image URL from the internet
Figure 115. Dynamically fetching an image URL from the internet [4]

This fetches the images in the URL asynchronously and fires a data change event when the data arrives to automatically refresh the ImageViewer when that happens.

8.26. ScaleImageLabel & ScaleImageButton

ScaleImageLabel & ScaleImageButton allow you to position an image that will grow/shrink to fit available space. In that sense they differ from Label & Button which keeps the image at the same size.

The default UIID of ScaleImageLabel is Label, but the default UIID of ScaleImageButton is ScaleImageButton. The reasoning for the difference is that the Button UIID includes a border and a lot of legacy.

You can use ScaleImageLabel/ScaleImageButton interchangeably. The major difference between these components is the buttons ability to handle click events/focus.

Here is a simple example that also shows the difference between the scale to fill and scale to fit modes:

TableLayout tl = new TableLayout(2, 2);
Form hi = new Form("ScaleImageButton/Label", tl);
Style s = UIManager.getInstance().getComponentStyle("Button");
Image icon = FontImage.createMaterial(FontImage.MATERIAL_WARNING, s);
ScaleImageLabel fillLabel = new ScaleImageLabel(icon);
fillLabel.setBackgroundType(Style.BACKGROUND_IMAGE_SCALED_FILL);
ScaleImageButton fillButton = new ScaleImageButton(icon);
fillButton.setBackgroundType(Style.BACKGROUND_IMAGE_SCALED_FILL);
hi.add(tl.createConstraint().widthPercentage(20), new ScaleImageButton(icon)).
        add(tl.createConstraint().widthPercentage(80), new ScaleImageLabel(icon)).
        add(fillLabel).
        add(fillButton);
hi.show();
ScaleImageLabel/Button the top row includes scale to fit versions (the default) whereas the bottom row includes the scale to fill versions
Figure 116. ScaleImageLabel/Button, the top row includes scale to fit versions (the default) whereas the bottom row includes the scale to fill versions
When styling these components keep in mind that changing attributes such as background behavior might cause an issue with their functionality or might not work.

8.27. Toolbar

The Toolbar API provides deep customization of the title bar area with more flexibility for example: placing a TextField for search or buttons in arbitrary title area positions. The Toolbar API replicates some native functionality available on Android/iOS and integrates with features such as the side menu to provide fine grained control over the title area behavior.

The Toolbar needs to be installed into the Form in order for it to work. You can setup the Toolbar in one of these three ways:

  1. form.setToolbar(new Toolbar()); - allows you to activate the Toolbar to a specific Form and not for the entire application

  2. Toolbar.setGlobalToolbar(true); - enables the Toolbar for all the forms in the app

  3. Theme constant globalToobarBool - this is equivalent to Toolbar.setGlobalToolbar(true);

The basic functionality of the Toolbar includes the ability to add a command to the following 4 places:

  • Left side of the title - addCommandToLeftBar

  • Right side of the title - addCommandToRightBar

  • Side menu bar (the drawer that opens when you click the icon on the top left or swipe the screen from left to right) - addCommandToSideMenu

  • Overflow menu (the menu that opens when you tap the 3 vertical dots in the top right corner) - addCommandToOverflowMenu

The code below provides a brief overview of these options:

Style s = UIManager.getInstance().getComponentStyle("TitleCommand");
Image icon = FontImage.createMaterial(FontImage.MATERIAL_INFO, s);
Toolbar.setGlobalToolbar(true);

Form hi = new Form("Toolbar", new BoxLayout(BoxLayout.Y_AXIS));
hi.getToolbar().addCommandToLeftBar("Left", icon, (e) -> Log.p("Clicked"));
hi.getToolbar().addCommandToRightBar("Right", icon, (e) -> Log.p("Clicked"));
hi.getToolbar().addCommandToOverflowMenu("Overflow", icon, (e) -> Log.p("Clicked"));
hi.getToolbar().addCommandToSideMenu("Sidemenu", icon, (e) -> Log.p("Clicked"));
hi.show();
The Toolbar
Figure 117. The Toolbar
The default sidemenu of the Toolbar
Figure 118. The default sidemenu of the Toolbar
The overflow menu of the Toolbar
Figure 119. The overflow menu of the Toolbar

You can set a title with a String but if you would want the component to be a text field or a multi line label you can use setTitleComponent(Component) which allows you to install any component into the title area.

The code below demonstrates searching using custom code but the Toolbar also has built-in support for search covered in the next section

The customization of the title area allows for some pretty powerful UI effects for example: the code below allows searching dynamically within a set of entries and uses some neat tricks:

Toolbar.setGlobalToolbar(true);
Style s = UIManager.getInstance().getComponentStyle("Title");

Form hi = new Form("Toolbar", new BoxLayout(BoxLayout.Y_AXIS));
TextField searchField = new TextField("", "Toolbar Search"); (1)
searchField.getHintLabel().setUIID("Title");
searchField.setUIID("Title");
searchField.getAllStyles().setAlignment(Component.LEFT);
hi.getToolbar().setTitleComponent(searchField);
FontImage searchIcon = FontImage.createMaterial(FontImage.MATERIAL_SEARCH, s);
searchField.addDataChangeListener((i1, i2) -> { (2)
    String t = searchField.getText();
    if (t.length() < 1) {
        for (Component cmp : hi.getContentPane()) {
            cmp.setHidden(false);
            cmp.setVisible(true);
        }
    } else {
        String needle = t.toLowerCase();
        for (Component cmp : hi.getContentPane()) {
            String val;
            if (cmp instanceof Label) {
                val = ((Label) cmp).getText();
            } else {
                if (cmp instanceof TextArea) {
                    val = ((TextArea) cmp).getText();
                } else {
                    val = (String) cmp.getPropertyValue("text");
                }
            }
            boolean show = val != null && val.toLowerCase().indexOf(needle) > -1;
            cmp.setHidden(!show);
            cmp.setVisible(show); (3)
        }
    }
    hi.getContentPane().animateLayout(250);
});
hi.getToolbar().addCommandToRightBar("", searchIcon, (e) -> { (4)
    searchField.startEditingAsync();
});

hi.add("A Game of Thrones").
        add("A Clash Of Kings").
        add("A Storm Of Swords").
        add("A Feast For Crows").
        add("A Dance With Dragons").
        add("The Winds of Winter").
        add("A Dream of Spring");
hi.show();
1You use a TextField the whole time and style it to make it (and its hint) look like a regular title. An alternative way is to replace the title component dynamically.
2You use the DataChangedListener to update the search results as you type them.
3Hidden & Visible use the opposite flag values to say similar things (for example: when hidden is set to false you would want to set visible to true).
Visible indicates whether a component can be seen. It will still occupy the physical space on the screen even when it isn’t visible. Hidden will remove the space occupied by the component from the screen, but some code might still try to paint it., visible is redundant but you use it with hidden for good measure.
4The search button is totally unnecessary here. You can click the TextField!
For example, that isn’t intuitive to most users so a button is added to start editing.
Search field within the toolbar
Figure 120. Search field within the toolbar
Search field after typing a couple of letters
Figure 121. Search field after typing a couple of letters

8.27.1. Search mode

While you can implement search manually, using the built-in search offers a simpler and uniform UI.

built-in toolbar search functionality
Figure 122. built-in toolbar search functionality

You can customize the appearance of the search bar by using the UIID’s: ToolbarSearch, TextFieldSearch & TextHintSearch.

In the sample below you fetch all the contacts from the device and enable search through them, notice it expects and image called duke.png which is the default Codename One icon renamed and placed in the src folder:

int fiveMM = Display.getInstance().convertToPixels(5);
// a material icon, so the sample needs no bundled asset
final Image finalDuke = FontImage.createMaterial(FontImage.MATERIAL_PORTRAIT, "Label", 3.0f)
        .scaledWidth(fiveMM);
Toolbar.setGlobalToolbar(true);
Form hi = new Form("Search", BoxLayout.y());
hi.add(new InfiniteProgress());
Display.getInstance().scheduleBackgroundTask(()-> {
    // this will take a while...
    Contact[] cnts = Display.getInstance().getAllContacts(true, true, true, true, false, false);
    Display.getInstance().callSerially(() -> {
        hi.removeAll();
        if(cnts == null) {
            // the platform has no contacts API, or access was refused
            hi.add(new Label("Contacts are unavailable"));
            hi.revalidate();
            return;
        }
        for(Contact c : cnts) {
            MultiButton m = new MultiButton();
            m.setTextLine1(c.getDisplayName());
            m.setTextLine2(c.getPrimaryPhoneNumber());
            Image pic = c.getPhoto();
            if(pic != null) {
                m.setIcon(pic.fill(finalDuke.getWidth(), finalDuke.getHeight()));
            } else {
                m.setIcon(finalDuke);
            }
            hi.add(m);
        }
        // the query may have been typed while the contacts were loading
        filterContacts(hi, currentSearch);
        hi.revalidate();
    });
});

hi.getToolbar().addSearchCommand(e -> {
    currentSearch = (String)e.getSource();
    filterContacts(hi, currentSearch);
}, 4);

hi.show();

The filter itself, shared by the search command and the load callback so a query typed while the contacts are still arriving is reapplied once they do:

String currentSearch;

void filterContacts(Form hi, String text) {
    if(text == null || text.length() == 0) {
        // clear search
        for(Component cmp : hi.getContentPane()) {
            cmp.setHidden(false);
            cmp.setVisible(true);
        }
        hi.getContentPane().animateLayout(150);
        return;
    }
    text = text.toLowerCase();
    for(Component cmp : hi.getContentPane()) {
        if(!(cmp instanceof MultiButton)) {
            // the loading indicator is still there until the contacts arrive
            continue;
        }
        MultiButton mb = (MultiButton)cmp;
        String line1 = mb.getTextLine1();
        String line2 = mb.getTextLine2();
        boolean show = line1 != null && line1.toLowerCase().indexOf(text) > -1 ||
                line2 != null && line2.toLowerCase().indexOf(text) > -1;
        mb.setHidden(!show);
        mb.setVisible(show);
    }
    hi.getContentPane().animateLayout(150);
}

8.27.2. South Component

A common feature in side menu bar is the ability to add a component to the "south" part of the side menu.

Notice that this feature works with the on-top and permanent versions of the side menu and not with the legacy versions:

toolbar.setComponentToSideMenuSouth(myComponent);

This places the component below the side menu bar. Notice that this component controls its entire UIID & is separate from the SideNavigationPanel UIID so if you set that component you might want to place it within a container that has the SideNavigationPanel UIID so it will blend with the rest of the UI.

8.27.3. Title animations

Modern UI’s often animate the title upon scrolling to balance the highly functional smaller title advantage with the gorgeous large image based title. This is pretty easy to do with the Toolbar API through the Title animation API.

The code below shows off an attractive title based on a book by GRRM on top of text [5] that’s scrollable. As the text is scrolled the title fades out:

Toolbar.setGlobalToolbar(true);

Form hi = new Form("Toolbar", new BoxLayout(BoxLayout.Y_AXIS));
EncodedImage placeholder = EncodedImage.createFromImage(Image.createImage(hi.getWidth(), hi.getWidth() / 5, 0xffff0000), true);
URLImage background = URLImage.createToStorage(placeholder, "400px-AGameOfThrones.jpg",
        "http://awoiaf.westeros.org/images/thumb/9/93/AGameOfThrones.jpg/400px-AGameOfThrones.jpg");
background.fetch();
Style stitle = hi.getToolbar().getTitleComponent().getUnselectedStyle();
stitle.setBgImage(background);
stitle.setBackgroundType(Style.BACKGROUND_IMAGE_SCALED_FILL);
stitle.setPaddingUnit(Style.UNIT_TYPE_DIPS, Style.UNIT_TYPE_DIPS, Style.UNIT_TYPE_DIPS, Style.UNIT_TYPE_DIPS);
stitle.setPaddingTop(15);
SpanButton credit = new SpanButton("This excerpt is from A Wiki Of Ice And Fire. Please check it out by clicking here!");
credit.addActionListener((e) -> Display.getInstance().execute("http://awoiaf.westeros.org/index.php/A_Game_of_Thrones"));
hi.add(new SpanLabel("A Game of Thrones is the first of seven planned novels in A Song of Ice and Fire, an epic fantasy series by American author George R. R. Martin. It was first published on 6 August 1996. The novel was nominated for the 1998 Nebula Award and the 1997 World Fantasy Award,[1] and won the 1997 Locus Award.[2] The novella Blood of the Dragon, comprising the Daenerys Targaryen chapters from the novel, won the 1997 Hugo Award for Best Novella. ")).
        add(new Label("Plot introduction", "Heading")).
        add(new SpanLabel("A Game of Thrones is set in the Seven Kingdoms of Westeros, a land reminiscent of Medieval Europe. In Westeros the seasons last for years, sometimes decades, at a time.\n\n" +
            "Fifteen years prior to the novel, the Seven Kingdoms were torn apart by a civil war, known alternately as \"Robert's Rebellion\" and the \"War of the Usurper.\" Prince Rhaegar Targaryen kidnapped Lyanna Stark, arousing the ire of her family and of her betrothed, Lord Robert Baratheon (the war's titular rebel). The Mad King, Aerys II Targaryen, had Lyanna's father and eldest brother executed when they demanded her safe return. Her second brother, Eddard, joined his boyhood friend Robert Baratheon and Jon Arryn, with whom they had been fostered as children, in declaring war against the ruling Targaryen dynasty, securing the allegiances of House Tully and House Arryn through a network of dynastic marriages (Lord Eddard to Catelyn Tully and Lord Arryn to Lysa Tully). The powerful House Tyrell continued to support the King, but House Lannister and House Martell both stalled due to insults against their houses by the Targaryens. The civil war climaxed with the Battle of the Trident, when Prince Rhaegar was killed in battle by Robert Baratheon. The Lannisters finally agreed to support King Aerys, but then brutally... ")).
        add(credit);

ComponentAnimation title = hi.getToolbar().getTitleComponent().createStyleAnimation("Title", 200);
hi.getAnimationManager().onTitleScrollAnimation(title);
hi.show();
The Toolbar starts with the large URLImage fetched from the web
Figure 123. The Toolbar starts with the large URLImage fetched from the web
As you scroll down the image fades and the title shrinks in size returning to the default UIID look
Figure 124. As you scroll down the image fades and the title shrinks in size returning to the default UIID look
As scrolling continues the title reaches standard size
Figure 125. As scrolling continues the title reaches standard size

Most the code above creates the "look" of the application. The key piece of code above is this:

ComponentAnimation title = hi.getToolbar().getTitleComponent().createStyleAnimation("Title", 200);
hi.getAnimationManager().onTitleScrollAnimation(title);

In the first line you create a style animation that will translate the style from the current settings to the destination UIID (the first argument) within 200 pixels of scrolling. You then bind this animation to the title scrolling animation event.

8.28. BrowserComponent & WebBrowser

BrowserComponent is a peer component, understanding this is crucial if your application depends on such a component. You can learn about peer components and their issues here.

The WebBrowser component shows the native device web browser when supported by the device and the HTMLComponent when the web browser isn’t supported on the given device. If you intend to target smartphones you should use the BrowserComponent directly instead of the WebBrowser.

The BrowserComponent can point at an arbitrary URL to load it:

Form hi = new Form("Browser", new BorderLayout());
BrowserComponent browser = new BrowserComponent();
browser.setURL("https://www.codenameone.com/");
hi.add(BorderLayout.CENTER, browser);
Browser Component showing the Codename One website on the simulator
Figure 126. Browser Component showing the Codename One website on the simulator
The scrollbars appear in the simulator, device versions of the browser component act differently and support touch scrolling.
A BrowserComponent should be in the center of a BorderLayout. Otherwise its preferred size might be zero before the HTML finishes loading/layout in the native layer and layout might be wrong as a result.

You can use WebBrowser and BrowserComponent interchangeably for most basic usage. For example, if you need access to JavaScript or native browser functionality then there is no use in going through the WebBrowser abstraction.

The BrowserComponent has full support for executing local web pages from within the jar. The basic support uses the jar:/// URL as such:

BrowserComponent wb = new BrowserComponent();
wb.setURL("jar:///Page.html");
On Android a native indicator might show up when the web page is loading. This can be disabled using the Display.getInstance().setProperty("WebLoadingHidden", "true"); call. You need to invoke this once.

8.28.1. Troubleshooting "Failed to create CEF browser" on Linux

In the Simulator, BrowserComponent is rendered by the bundled CEF (Chromium Embedded Framework) port. On Linux, BrowserComponent.isNativeBrowserSupported() may return true and yet creating the component throws RuntimeException: Failed to create CEF browser, with a root cause similar to:

java.lang.UnsatisfiedLinkError: .../codenameone-cef-*-linux64.zip-extracted/lib/linux64/libjcef.so:
    .../lib/linux64/libjawt.so: version `SUNWprivate_1.1' not found
    (required by .../lib/linux64/libjcef.so)

The native CEF library (libjcef.so) is loaded with a sibling libjawt.so shipped inside the CEF archive. That copy of libjawt.so expects symbols (SUNWprivate_1.1) provided by the JDK’s own libjawt.so and the matching libjvm.so. When the JVM running the Simulator doesn’t expose those symbols at load time — typically because the dynamic linker doesn’t have the JDK’s native library directories on its search path — the load fails before any browser is created. This is purely a Simulator/desktop concern; the same code runs unchanged on Android and iOS.

The fix is to launch the IDE (and therefore the Simulator JVM) with JAVA_HOME and LD_LIBRARY_PATH pointing at a supported JDK. For example, on Linux Mint / Ubuntu wrap the IDE startup script:

#!/bin/bash
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
export LD_LIBRARY_PATH=$JAVA_HOME/lib:$JAVA_HOME/lib/server${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}
exec /path/to/your/ide "$@"

After relaunching the IDE through this wrapper, BrowserComponent instantiation succeeds in the Simulator. Any JDK from 11 through 25 that’s supported for running the Simulator works — the important part is that LD_LIBRARY_PATH resolves to that JDK’s lib and lib/server directories so the bundled libjcef.so finds a compatible libjawt/libjvm pair.

If BrowserComponent instantiation fails on Linux, log the environment with CN.getProperty("java.home", "<unset>"), CN.getProperty("JAVA_HOME", "<unset>") and CN.getProperty("LD_LIBRARY_PATH", "<unset>"). An <unset> value for LD_LIBRARY_PATH while the IDE was launched from a desktop shortcut is the typical fingerprint of this problem — desktop launchers don’t source your shell profile.
This issue doesn’t affect packaged native applications, only the desktop Simulator running on Linux.

8.28.2. BrowserComponent hierarchy

When Codename One packages applications into native apps it hides a lot of details to make the process simpler. One of the things hidden is the fact that you aren’t dealing with a JAR anymore, so getResource/getResourceAsStream are problematic…​ Both of these APIs support hierarchies and a concept of package relativity both of which might not be supported on all OSes.

Codename One has its own getResourceAsSteam in the Display class and that works fine, but it requires that all files be in the src root directory.

That’s why recommend that you place files inside res files. A resource file allows you to add arbitrary data files and you can have as many resource files as you need.

For web developers this isn’t enough since hierarchies are used often to represent the various dependencies, this means that many links & references are relative. To work with such hierarchies place all your resources in a hierarchy under the HTML package in the project source directory (src/html). The build server will tar the entire content of that package and add an html.tar file into the native package. This tar is seamlessly extracted on the device when you actually need the resources and with new application versions (not on every launch). Assuming the resources are under the HTML root package they can be displayed with code like this:

try {
    browserComponent.setURLHierarchy("/htmlFile.html");
} catch(IOException err) {
    /* omitted */
}

Notice that the path is relative to the HTML directory and starts with / but inside the HTML files you should use relative (not absolute) paths.

Also notice that an IOException can be thrown due to the process of untarring. Its unlikely to happen but is entirely possible.

8.28.3. NavigationCallback

At the core of the BrowserComponent you have the BrowserNavigationCallback. It might not seem like the most important interface within the browser but it’s the "glue" that allows the JavaScript code to communicate back into the Java layer.

You can bind a BrowserNavigationCallback by invoking setBrowserNavigationCallback on the BrowserComponent. At that point with every navigation within the browser the callback will get invoked.

The shouldNavigate method from the BrowserNavigationCallback is invoked in a native thread and NOT ON THE EDT!
it’s crucial that this method returns and that it won’t do any changes on the UI.

The shouldNavigate indicates to the native code whether navigation should proceed or not. For example: if a user clicks a specific link you might choose to do something in the Java code so you can return false and block the navigation. You can invoke callSerially to do the actual task in the Java side:

Form hi = new Form("BrowserComponent", new BorderLayout());
BrowserComponent bc = new BrowserComponent();
bc.setPage( "<html lang=\"en\">\n" +
            "    <head>\n" +
            "        <meta charset=\"utf-8\">\n" +
            "        <script>\n" +
            "          function  fnc(message) {\n" +
            "         document.write(message);\n" +
            "            };\n" +
            "        </script>\n" +
            "    </head>\n" +
            "    <body >\n" +
            "        <p><a href=\"http://click\">Demo</a></p>\n" +
            "    </body>\n" +
            "</html>", null);
hi.add(BorderLayout.CENTER, bc);
bc.setBrowserNavigationCallback((url) -> {
    if(url.startsWith("http://click")) {
        Display.getInstance().callSerially(() -> bc.execute("fnc('<p>You clicked!</p>')"));
        return false;
    }
    return true;
});
Before the link is clicked for the "shouldNavigate" call
Figure 127. Before the link is clicked for the "shouldNavigate" call
After the link is clicked for the "shouldNavigate" call
Figure 128. After the link is clicked for the "shouldNavigate" call
The JavaScript Bridge is implemented on top of the BrowserNavigationCallback.

8.28.4. JavaScript

The JavaScript bridge is sometimes confused with the JavaScript Port. The JavaScript bridge allows you to communicate with JavaScript from Java (and vice versa). The JavaScript port allows you to compile the Codename One application into a JavaScript application that runs in a standard web browser without code changes (think GWT without source changes and with thread support).+ You discuss the JavaScript port further later in the guide.

Use the BrowserComponent API to interact with JavaScript. It replaces the deprecated com.codename1.javascript package.

What was wrong with the old API

The old API provided a synchronous wrapper around an inherently asynchronous process, and made extensive use of invokeAndBlock() underneath the covers. This resulted in a nice API with high-level abstractions that played with a synchronous programming model, but it came with a price-tag in performance, complexity, and predictability. Let’s take a simple example, getting a reference to the "window" object:

JSObject window = (JSObject)ctx.get("window");

This code looks harmless enough, but this is actually expensive. It issues a command to the BrowserComponent, and uses invokeAndBlock() to wait for the command to go through and send back a response. invokeAndBlock() is a magical tool that allows you to "block" without blocking the EDT, but it has its costs, and shouldn’t be overused. Most of the Codename One APIs that use invokeAndBlock() show this in their name. For example: Component.animateLayoutAndWait(). This provides you the expectation that this call could take some time, and helps to alert you to the underlying cost.

The problem with the ctx.get("window") call is that it looks the same as a call to Map.get(key). There’s no sign that this call is expensive and could take time. One call like this probably isn’t a big deal, but it doesn’t take long before you have dozens or even hundreds of calls like this littered throughout your codebase, and they can be hard to pick out.

The new API

The new API fully embraces the asynchronous nature of JavaScript. It uses callbacks instead of return values, and provides convenience wrappers with the appropriate "AndWait()" naming convention to allow for synchronous usage. Let’s look at a simple example:

In all the sample code below, you can assume that variables named bc represent an instance of BrowserComponent:
bc.execute(
    "callback.onSuccess(3+4)",
    res -> Log.p("The result was "+res.getInt())
);

This code should output "The result was 7" to the console. It’s fully asynchronous, so you can include this code anywhere without worrying about it "bogging down" your code. The full signature of this form of the execute() method is:

public void execute(String js, SuccessCallback<JSRef> callback)

The first parameter is a JavaScript expression. This JavaScript MUST call either callback.onSuccess(result) or callback.onError(message, errCode) at some point in order for your callback to be called.

The second parameter is your callback that’s executed from the JavaScript side, when callback.onSuccess(res) is called. The callback takes a single parameter of type JSRef which is a generic wrapper around a JavaScript variable. JSRef has accessors to retrieve the value as some primitive types. For example: getBoolean(), getDouble(), getInt(), toString(), and it provides some introspection through the getType() method.

It’s worth noting that the callback method can take a single parameter. If you need to pass many parameters, you may consider including them in a single string which you parse in your callback.
Synchronous wrappers

As mentioned before, the new API also provides an executeAndWait() wrapper for execute() that will work synchronously. It, as its name suggests, uses invokeAndBlock under the hood so as not to block the EDT while it’s waiting.

For example:

JSRef res = bc.executeAndWait("callback.onSuccess(3+4)");
Log.p("The result was "+res.getInt());

Prints The result was 7.

When using the andWait() variant, it’s critical that your JavaScript calls your callback method at some point - otherwise it will block indefinitely. Codename One provides variants of executeAndWait() that include a timeout in case you want to hedge against this possibility.
Multi-use callbacks

The callbacks you pass to execute() and executeAndWait() are single-use callbacks. You can’t, for example, store the callback variable on the JavaScript side for later use (for example: to respond to a button click event). If you need a "multi-use" callback, you should use the addJSCallback() method instead. Its usage looks identical to execute(), the difference is that the callback will live on after its first use. For example: Consider the following code:

bc.execute(
    "$('#somebutton').click(function(){callback.onSuccess('Button was clicked')})",
    res -> Log.p(res.toString())
);

The above example, assumes that jQuery is loaded in the webpage that you’re interacting with, and you’re adding a click handler to a button with ID "somebutton." The click handler calls your callback.

If you run this example, the first time the button is clicked, you’ll see "Button was clicked" printed to the console as expected. For example, the 2nd time, you’ll get an exception. This is because the callback passed to execute() is single-use.

You need to change this code to use the addJSCallback() method as follows:

bc.addJSCallback(
    "$('#somebutton').click(function(){callback.onSuccess('Button was clicked')})",
    res -> Log.p(res.toString())
);

Now it will work no matter how many times the button is clicked.

Passing parameters to JavaScript

Often, the JavaScript expressions that you execute will include parameters from your Java code. Escaping these parameters is tricky at worst, and annoying at best. For example: If you’re passing a string, you need to make sure that it escapes quotes and new lines or it will cause the JavaScript to have a syntax error. Codename One provides variants of execute() and addJSCallback() that allow you to pass your parameters and have them automatically escaped.

For example, suppose you want to pass a string with text to set in a textarea within the webpage. You can do something like:

bc.execute(
    "jQuery('#bio').text(${0}); jQuery('#age').text(${1})",
    new Object[]{
       "A multi-line\n string with \"quotes\"",
       27
    }
);

The gist is that you embed placeholders in the JavaScript expression that are replaced by the corresponding entry in an array of parameters. The ${0} placeholder is replaced by the first item in the parameters array, the ${1} placeholder is replaced by the 2nd, etc.

Proxy objects

The new API also includes a JSProxy class that encapsulates a JavaScript object simplify the getting and setting of properties on JavaScript objects - and the calling of their methods. It provides essentially three core methods, along with many variants of each to allow for async or synchronous usages, parameters, and timeouts.

For example: You might want to create a proxy for the window.location object so that you can access its properties more from Java:

JSProxy location = bc.createJSProxy("window.location");

Then you can retrieve its properties using the get() method:

location.get("href", res -> Log.p("location.href="+res));

Or synchronously:

JSRef href = location.getAndWait("href");
Log.p("location.href="+href);

You can also set its properties:

location.set("href", "https://www.google.com");

And call its methods:

location.call("replace", new Object[]{"https://www.google.com"},
    res -> Log.p("Return value was "+res)
);
Legacy JSObject support

This section describes the now deprecated JSObject approach. It’s here for reference by developers working with older code. Use the new API when starting a new project.

BrowserComponent can communicate with the HTML code using JavaScript calls. For example: you can create HTML like this:

Form hi = new Form("BrowserComponent", new BorderLayout());
BrowserComponent bc = new BrowserComponent();
bc.setPage( "<html lang=\"en\">\n" +
            "    <head>\n" +
            "        <meta charset=\"utf-8\">\n" +
            "        <script>\n" +
            "          function  fnc(message) {\n" +
            "         document.write(message);\n" +
            "            };\n" +
            "        </script>\n" +
            "    </head>\n" +
            "    <body >\n" +
            "        <p>Demo</p>\n" +
            "    </body>\n" +
            "</html>", null);
TextField tf = new TextField();
hi.add(BorderLayout.CENTER, bc).
        add(BorderLayout.SOUTH, tf);
bc.addWebEventListener("onLoad", (e) ->  bc.execute("fnc('<p>Hello World</p>')"));
tf.addActionListener((e) -> bc.execute("fnc('<p>" + tf.getText() +"</p>')"));
hi.show();
JavaScript code was invoked to append text into the browser image above
Figure 129. JavaScript code was invoked to append text into the browser image above
Notice that opening an alert in an embedded native browser might not work

You use the execute method above to execute custom JavaScript code. You also have an executeAndReturnString method that allows you to receive a response value from the JavaScript side.

Coupled with shouldNavigate you can effectively do everything which is what the JavaScript Bridge tries to do.

The JavaScript bridge

While it’s possible to build everything on top of execute and shouldNavigate, both of these methods have their limits. That’s why Codename One introduced the JavaScript package, it allows you to communicate with JavaScript using intuitive code/syntax.

The JavascriptContext class lays the foundation by enabling you to call JavaScript code directly from Java. It provides automatic type conversion between Java and JavaScript types as follows:

Table 4. Java to JavaScript
Java TypeJavaScript Type

String

String

Double/Integer/Float/Long

Number

Boolean

Boolean

JSObject

Object

null

null

Other

Not Allowed

Table 5. JavaScript to Java
JavaScript TypeJava Type

String

String

Number

Double

Boolean

Boolean

Object

JSObject

Function

JSObject

Array

JSObject

null

null

undefined

null

This conversion table is more verbose than necessary, since JavaScript functions and arrays are, in fact Objects themselves, so those rows are redundant. All JavaScript objects are converted to JSObject.

You can access JavaScript variables from the context by using code like this:

Form hi = new Form("BrowserComponent", new BorderLayout());
BrowserComponent bc = new BrowserComponent();
bc.setPage( "<html lang=\"en\">\n" +
            "    <head>\n" +
            "        <meta charset=\"utf-8\">\n" +
            "    </head>\n" +
            "    <body >\n" +
            "        <p>This will appear twice...</p>\n" +
            "    </body>\n" +
            "</html>", null);
hi.add(BorderLayout.CENTER, bc);
bc.addWebEventListener("onLoad", (e) -> {
    // Create a JavaScript context for this BrowserComponent
    JavascriptContext ctx = new JavascriptContext(bc);

    String pageContent = (String)ctx.get("document.body.innerHTML");
    hi.add(BorderLayout.SOUTH, pageContent);
    hi.revalidate();
});
hi.show();
The contents were copied from the DOM and placed in the south position of the form
Figure 130. The contents were copied from the DOM and placed in the south position of the form

Notice that when you work with numeric values or anything related to the types mentioned above your code must be aware of the typing. For example: in this case the type is Double and not String:

Double outerWidth = (Double)ctx.get("window.outerWidth");

You can also query the context for objects and change their value for example:

Form hi = new Form("BrowserComponent", new BorderLayout());
BrowserComponent bc = new BrowserComponent();
bc.setPage( "<html lang=\"en\">\n" +
            "    <head>\n" +
            "        <meta charset=\"utf-8\">\n" +
            "    </head>\n" +
            "    <body >\n" +
            "        <p>Please Wait...</p>\n" +
            "    </body>\n" +
            "</html>", null);
hi.add(BorderLayout.CENTER, bc);
bc.addWebEventListener("onLoad", new ActionListener() {
    public void actionPerformed(ActionEvent e) {
        // the destination fires onLoad too, so stop listening before navigating
        bc.removeWebEventListener("onLoad", this);

        // Create a JavaScript context for this BrowserComponent
        JavascriptContext ctx = new JavascriptContext(bc);

        JSObject jo = (JSObject)ctx.get("window");
        jo.set("location", "https://www.codenameone.com/");
    }
});
hi.show();

This code effectively navigates to the Codename One home page by fetching the DOM’s window object and setting its location property to https://www.codenameone.com/.

8.28.5. Cordova/PhoneGap integration

PhoneGap was one of the first web app packager tools in the market: a tool that’s effectively a browser component within a native wrapper coupled with native access APIs, and Cordova is the open source extension of this popular project.

Codename One supports embedding PhoneGap/Cordova applications directly into Codename One applications. This is easy to do with the BrowserComponent and JavaScript integration. The main aspect that this integration requires is support for Cordova plugins & its JavaScript APIs.

The effort to integrate Cordova/PhoneGap support into Codename One is handled within an open-source GitHub project here. The chief benefits of picking Codename One rather than using Cordova directly are:

  • Build Cloud

  • Better Native Code Support

  • Better Protection Of IP

  • IDE Integration Java - JavaScript - HTML

  • Easy, doesn’t Require A Mac, Automates Certificates/Signing

  • Migration To Java

This is discussed further in the original announcement.

8.29. Text editors

RichTextArea and CodeEditor are introduced here as components, but editing is broad enough to have its own chapter. See Chapter 10, Rich Text and Code Editing for rich HTML, syntax highlighting, completion, diagnostics, touch and desktop selection, scrolling, bidirectional text, input methods, backend selection, and the port-level TextInputClient contract.

8.30. AutoCompleteTextField

The AutoCompleteTextField allows you to write text into a text field and select a completion entry from the list in a similar way to a search engine.

This is easy to incorporate into your code, replace your usage of TextField with AutoCompleteTextField and define the data that the autocomplete should work from. A default implementation accepts a String array or a ListModel for completion strings, this can work well for a "small" set of thousands (or tens of thousands) of entries.

For example: This is a trivial use case that can work well for smaller sample sizes:

Form hi = new Form("Auto Complete", new BoxLayout(BoxLayout.Y_AXIS));
AutoCompleteTextField ac = new AutoCompleteTextField("Short", "Shock", "Sholder", "Shrek");
ac.setMinimumElementsShownInPopup(5);
hi.add(ac);
Autocomplete Text Field
Figure 131. Autocomplete Text Field

For example, if you wish to query a database or a web service you will need to derive the class and perform more advanced filtering by overriding the filter method:

  Form hi = new Form("Autocomplete", new BoxLayout(BoxLayout.Y_AXIS));
public void showForm() {
  final DefaultListModel<String> options = new DefaultListModel<>();
  AutoCompleteTextField ac = new AutoCompleteTextField(options) {
      @Override
      protected boolean filter(String text) {
          if(text.length() == 0) {
              // an emptied field must not keep showing the last query's matches
              options.removeAll();
              return false;
          }
          String[] l = searchLocations(text);
          if(!text.equals(getText())) {
              // the wait above pumps the EDT, so the field may have moved on
              // and a newer query may already have filled the model
              return false;
          }
          if(l == null || l.length == 0) {
              // otherwise the popup keeps showing the previous query's matches
              options.removeAll();
              return false;
          }

          options.removeAll();
          for(String s : l) {
              options.addItem(s);
          }
          return true;
      }

  };
  ac.setMinimumElementsShownInPopup(5);
  hi.add(ac);
  hi.add(new SpanLabel("This demo requires a valid google API key to be set below "
           + "you can get this key for the webservice (not the native key) by following the instructions here: "
           + "https://developers.google.com/places/web-service/get-api-key"));
  hi.add(apiKey);
  hi.getToolbar().addCommandToRightBar("Get Key", null, e -> Display.getInstance().execute("https://developers.google.com/places/web-service/get-api-key"));
  hi.show();
}

TextField apiKey = new TextField();

String[] searchLocations(String text) {
    try {
        if(text.length() > 0) {
            ConnectionRequest r = new ConnectionRequest();
            r.setPost(false);
            r.setUrl("https://maps.googleapis.com/maps/api/place/autocomplete/json");
            r.addArgument("key", apiKey.getText());
            r.addArgument("input", text);
            NetworkManager.getInstance().addToQueueAndWait(r);
            Map<String,Object> result = new JSONParser().parseJSON(new InputStreamReader(new ByteArrayInputStream(r.getResponseData()), "UTF-8"));
            String[] res = Result.fromContent(result).getAsStringArray("//description");
            return res;
        }
    } catch(Exception err) {
        Log.e(err);
    }
    return null;
}
Autocomplete Text Field with a webservice
Figure 132. Autocomplete Text Field with a webservice

8.30.1. Using images in AutoCompleteTextField

One question that comes up frequently is "How do you customize the results of the auto complete field"?

This sounds difficult to most people as you can work with Strings so how do you represent more data or format the date correctly?

The answer is actually pretty simple, you still need to work with Strings because autocomplete is fundamentally a text field. For example, that doesn’t preclude your custom renderer from fetching data that might be placed in a different location and associated with the result.

The following source code presents an autocomplete text field with images in the completion popup and two lines for every entry:

final String[] characters = { "Tyrion Lannister", "Jaime Lannister", "Cersei Lannister", "Daenerys Targaryen",
    "Jon Snow", "Petyr Baelish", "Jorah Mormont", "Sansa Stark", "Arya Stark", "Theon Greyjoy"
    // snipped the rest for clarity
};

Form current = new Form("AutoComplete", BoxLayout.y());

AutoCompleteTextField ac = new AutoCompleteTextField(characters);

final int size = Display.getInstance().convertToPixels(7);
final EncodedImage placeholder = EncodedImage.createFromImage(Image.createImage(size, size, 0xffcccccc), true);

final String[] actors = { "Peter Dinklage", "Nikolaj Coster-Waldau", "Lena Headey"}; (1)
final Image[] pictures = {
    URLImage.createToStorage(placeholder, "tyrion","http://i.lv3.hbo.com/assets/images/series/game-of-thrones/character/s5/tyrion-lannister-512x512.jpg"),
    URLImage.createToStorage(placeholder, "jaime","http://i.lv3.hbo.com/assets/images/series/game-of-thrones/character/s5/jamie-lannister-512x512.jpg"),
    URLImage.createToStorage(placeholder, "cersei","http://i.lv3.hbo.com/assets/images/series/game-of-thrones/character/s5/cersei-lannister-512x512.jpg")
};

ac.setCompletionRenderer(new ListCellRenderer() {
    private final Label focus = new Label(); (2)
    private final Label line1 = new Label(characters[0]);
    private final Label line2 = new Label(actors[0]);
    private final Label icon = new Label(pictures[0]);
    private final Container selection = BorderLayout.center(
            BoxLayout.encloseY(line1, line2)).add(BorderLayout.EAST, icon);

    @Override
    public Component getListCellRendererComponent(com.codename1.ui.List list, Object value, int index, boolean isSelected) {
        for(int iter = 0 ; iter < characters.length ; iter++) {
            if(characters[iter].equals(value)) {
                line1.setText(characters[iter]);
                if(actors.length > iter) {
                    line2.setText(actors[iter]);
                    icon.setIcon(pictures[iter]);
                } else {
                    line2.setText(""); (3)
                    icon.setIcon(placeholder);
                }
                break;
            }
        }
        return selection;
    }

    @Override
    public Component getListFocusComponent(com.codename1.ui.List list) {
        return focus;
    }
});
current.add(ac);

current.show();
1you have duplicate arrays that are partial for clarity. This is a separate list of data element but you can fetch the more data from anywhere
2You create the renderer UI instantly in the fields with the helper methods for wrapping elements which is pretty cool & terse
3In a renderer it’s important to always set the value if you don’t have a value in place
Auto complete with images
Figure 133. Auto complete with images

8.31. Picker

Picker occupies the limbo between native widget and lightweight widget. Picker is more like TextField/TextArea in the sense that it’s a Codename One widget that calls the native code during editing.

The reasoning for this is the highly native UX and functionality related to this widget type which should be obvious from the screenshots below.

At this time there are 4 types of pickers:

  • Time

  • Date & Time

  • Date

  • Strings

If a platform doesn’t support native pickers an internal fallback implementation is used. This is the implementation you always use in the simulator so assume different behavior when building for the device.

While Android supports Date, Time native pickers it doesn’t support the Date & Time native picker UX and will fall back in that case.

The sample below includes al picker types:

Form hi = new Form("Picker", new BoxLayout(BoxLayout.Y_AXIS));
Picker datePicker = new Picker();
datePicker.setType(Display.PICKER_TYPE_DATE);
Picker dateTimePicker = new Picker();
dateTimePicker.setType(Display.PICKER_TYPE_DATE_AND_TIME);
Picker timePicker = new Picker();
timePicker.setType(Display.PICKER_TYPE_TIME);
Picker stringPicker = new Picker();
stringPicker.setType(Display.PICKER_TYPE_STRINGS);
Picker durationPicker = new Picker();
durationPicker.setType(Display.PICKER_TYPE_DURATION);
Picker minuteDurationPicker = new Picker();
minuteDurationPicker.setType(Display.PICKER_TYPE_DURATION_MINUTES);
Picker hourDurationPicker = new Picker();
hourDurationPicker.setType(Display.PICKER_TYPE_DURATION_HOURS);

datePicker.setDate(new Date());
dateTimePicker.setDate(new Date());
timePicker.setTime(10 * 60); // 10:00AM = Minutes since midnight
stringPicker.setStrings("A Game of Thrones", "A Clash Of Kings", "A Storm Of Swords", "A Feast For Crows",
        "A Dance With Dragons", "The Winds of Winter", "A Dream of Spring");
stringPicker.setSelectedString("A Game of Thrones");

hi.add(datePicker).add(dateTimePicker).add(timePicker)
  .add(stringPicker).add(durationPicker)
  .add(minuteDurationPicker).add(hourDurationPicker);
hi.show();
The various picker components
Figure 134. The various picker components
The date & time picker on the simulator
Figure 135. The date & time picker on the simulator
The date picker component on the Android device
Figure 136. The date picker component on the Android device
Date and time picker on Android. Notice it didn’t use a built-in widget since there is none
Figure 137. Date & time picker on Android. Notice it didn’t use a built-in widget since there is none
String picker on the native Android device
Figure 138. String picker on the native Android device
Time picker on the Android device
Figure 139. Time picker on the Android device
Duration picker on Android device
Figure 140. Duration picker on Android device
Hours Duration picker on Android device
Figure 141. Hours Duration picker on Android device
Minutes Duration picker on Android device
Figure 142. Minutes Duration picker on Android device

The text displayed by the picker on selection is generated automatically by the updateValue() method. You can override it to display a custom formatted value and call setText(String) with the correct display string.

A common use case is to format date values based on a specific appearance and Picker has built-in support for a custom display formatter. Just use the setFormatter(SimpleDateFormat) method and set the appearance for the field.

When using lightweight picker mode (setUseLightweightPopup(true)), you can add custom quick-action buttons to the popup. This is useful for actions like setting the date to "Today" or "+7 Days" without scrolling the wheels manually:

Picker picker = new Picker();
picker.setType(Display.PICKER_TYPE_DATE);
picker.setUseLightweightPopup(true);
picker.setDate(new Date());

picker.addLightweightPopupButton("Today", () -> picker.setDate(new Date()));

picker.addLightweightPopupButton("+7 Days", () -> {
    Calendar cal = Calendar.getInstance();
    cal.add(Calendar.DAY_OF_MONTH, 7);
    picker.setDate(cal.getTime());
}, Picker.LightweightPopupButtonPlacement.BELOW_SPINNER);
Form hi = new Form("Picker", new BoxLayout(BoxLayout.Y_AXIS));
hi.add(picker);
hi.show();

Button placement options are:

  • Picker.LightweightPopupButtonPlacement.BETWEEN_CANCEL_AND_DONE (default)

  • Picker.LightweightPopupButtonPlacement.ABOVE_SPINNER

  • Picker.LightweightPopupButtonPlacement.BELOW_SPINNER

8.32. SwipeableContainer

The SwipeableContainer allows you to place a component such as a MultiButton on top of more "options" that can be exposed by swiping the component to the side.

This swipe gesture is commonly used in touch interfaces to expose features such as delete, edit etc. It’s trivial to use this component by determining the components placed on top and bottom (the revealed component):

SwipeableContainer swip = new SwipeableContainer(bottom, top);

You can combine some demos above including the Slider stars demo to rank GRRM’s books in an interactive way:

Form hi = new Form("Swipe", new BoxLayout(BoxLayout.Y_AXIS));
hi.add(createRankWidget("A Game of Thrones", "1996")).
    add(createRankWidget("A Clash Of Kings", "1998")).
    add(createRankWidget("A Storm Of Swords", "2000")).
    add(createRankWidget("A Feast For Crows", "2005")).
    add(createRankWidget("A Dance With Dragons", "2011")).
    add(createRankWidget("The Winds of Winter", "TBD")).
    add(createRankWidget("A Dream of Spring", "TBD"));
hi.show();
public SwipeableContainer createRankWidget(String title, String year) {
    MultiButton button = new MultiButton(title);
    button.setTextLine2(year);
    return new SwipeableContainer(FlowLayout.encloseCenterMiddle(createStarRankSlider()),
            button);
}
SwipeableContainer showing a common use case of ranking on swipe
Figure 143. SwipeableContainer showing a common use case of ranking on swipe

8.33. EmbeddedContainer

EmbeddedContainer solves a problem that exists within the GUI builder and the class makes no sense outside of the context of the GUI builder. The necessity for EmbeddedContainer came about due to iPhone inspired designs that relied on tabs (iPhone style tabs at the bottom of the screen) where different features of the application are within a different tab.

This didn’t mesh well with the GUI builder navigation logic and so it needed a rethink. The aim was to reuse GUI as much as possible while still enjoying the advantage of navigation being managed for you.

Android does this with Activities and the iPhone itself has a view controller, both approaches are problematic for Codename One. The problem is that you have what’s effectively two incompatible hierarchies to mix and match.

The Component/Container hierarchy is powerful enough to represent such a UI but you needed a "marker" to show to the UIBuilder where a "root" component exists so navigation occurs within the given "root." Here EmbeddedContainer comes into play, its a simple container that can contain another GUI from the GUI builder. Nothing else. You can place it in any form of UI and effectively have the UI change appropriately and navigation would default to "sensible values."

Navigation replaces the content of the embedded container; it finds the embedded container based on the component that broadcast the event. If you want to navigate manually use the showContainer() method which accepts a component, you can give any component that’s under the EmbeddedContainer you want to replace and Codename One will be smart enough to replace that component.

The nice part about using the EmbeddedContainer is that the resulting UI can be refactored to provide a more traditional form based UI without duplicating effort and can be adapted to a more tablet oriented UI (with a sidebar) again without much effort.

8.34. MapComponent

The MapComponent uses a somewhat outdated tiling API which isn’t as rich as modern native maps. Recommend using the GoogleMap’s Native cn1lib to integrate native mapping functionality into the Codename One app.

The MapComponent uses the OpenStreetMap webservice by default to display a navigatable map.

The code was contributed by Roman Kamyk and was originally used for a LWUIT application.

Map Component
Figure 144. Map Component

The screenshot above was produced using the following code:

Form map = new Form("Map");
map.setLayout(new BorderLayout());
map.setScrollable(false);
final MapComponent mc = new MapComponent();

try {
   //get the current location from the Location API
   Location loc = LocationManager.getLocationManager().getCurrentLocation();

   // null until the device has a fix, so the map opens without the marker
   if(loc != null) {
       Coord lastLocation = new Coord(loc.getLatitude(), loc.getLongtitude());
       // a material icon rather than an asset you would have to add
       Image i = FontImage.createMaterial(FontImage.MATERIAL_PLACE, "Label", 4.0f);
       PointsLayer pl = new PointsLayer();
       pl.setPointIcon(i);
       PointLayer p = new PointLayer(lastLocation, "You Are Here", i);
       p.setDisplayName(true);
       mc.addLayer(pl);
       pl.addPoint(p);
   }
} catch (IOException ex) {
   ex.printStackTrace();
}
mc.zoomToLayers();

map.addComponent(BorderLayout.CENTER, mc);
map.show();

The example below shows how to integrate the MapComponent with the Google Location API. Make sure to get your secret API key from the Google Location data API at: https://developers.google.com/maps/documentation/places/

MapComponent with Google Location API
Figure 145. MapComponent with Google Location API

8.35. Chart Component

The charts package enables Codename One developers to add charts and visualizations to their apps without having to include external libraries or embedding web views. You also wanted to harness the new features in the graphics pipeline to maximize performance.

8.35.1. Device support

Since the charts package makes use of 2D transformations and shapes, it requires some graphics features that aren’t yet available on all platforms. The following platforms are supported:

  1. Simulator

  2. Android

  3. iOS

8.35.2. Features

  1. Built-in support for many common types of charts including bar charts, line charts, stacked charts, scatter charts, pie charts and more.

  2. Pinch Zoom - The ChartComponent class includes optional pinch zoom support.

  3. Panning Support - The ChartComponent class includes optional support for panning.

8.35.3. Chart types

The com.codename1.charts package includes models and renderers for many different types of charts. It’s also extensible so that you can add your own chart types if required. The following screenshots show a small sampling of the types of charts that can be created.

Line Charts
Figure 146. Line Charts
Cubic Line Charts
Figure 147. Cubic Line Charts
Bar Charts
Figure 148. Bar Charts
Stacked Bar Charts
Figure 149. Stacked Bar Charts
Range Bar Charts
Figure 150. Range Bar Charts
Pie Charts
Figure 151. Pie Charts
Doughnut Charts
Figure 152. Doughnut Charts
Scatter Charts
Figure 153. Scatter Charts
Dial Charts
Figure 154. Dial Charts
Combined Charts
Figure 155. Combined Charts
Bubble Charts
Figure 156. Bubble Charts
Time Charts
Figure 157. Time Charts
The above screenshots were taken from the ChartsDemo app. You can start playing with this app by checking it out from your git repository.

8.35.4. How to create a chart

Adding a chart to your app involves four steps:

  1. Build the model. You can construct a model (aka data set) for the chart using one of the existing model classes in the com.codename1.charts.models package. Essentially, this is where you add the data that you want to display.

  2. Set up a renderer. You can create a renderer for your chart using one of the existing renderer classes in the com.codename1.charts.renderers package. The renderer allows you to specify how the chart should look. For example: the colors, fonts, styles, to use.

  3. Create the Chart View. Use one of the existing view classes in the com.codename1.charts.views package.

  4. Create a ChartComponent. To add your chart to the UI, you need to wrap it in a ChartComponent object.

You can check out the ChartsDemo app for specific examples, but here is a high-level view of some code that creates a Pie Chart:

/**
 * Creates a renderer for the specified colors.
 */
private DefaultRenderer buildCategoryRenderer(int[] colors) {
    DefaultRenderer renderer = new DefaultRenderer();
    renderer.setLabelsTextSize(15);
    renderer.setLegendTextSize(15);
    renderer.setMargins(new int[]{20, 30, 15, 0});
    for (int color : colors) {
        SimpleSeriesRenderer r = new SimpleSeriesRenderer();
        r.setColor(color);
        renderer.addSeriesRenderer(r);
    }
    return renderer;
}

/**
 * Builds a category series using the provided values.
 *
 * @param titles the series titles
 * @param values the values
 * @return the category series
 */
protected CategorySeries buildCategoryDataset(String title, double[] values) {
    CategorySeries series = new CategorySeries(title);
    int k = 0;
    for (double value : values) {
        series.add("Project " + ++k, value);
    }

    return series;
}

public Form createPieChartForm() {

    // Generate the values
    double[] values = new double[]{12, 14, 11, 10, 19};

    // Set up the renderer
    int[] colors = new int[]{ColorUtil.BLUE, ColorUtil.GREEN, ColorUtil.MAGENTA, ColorUtil.YELLOW, ColorUtil.CYAN};
    DefaultRenderer renderer = buildCategoryRenderer(colors);
    renderer.setZoomButtonsVisible(true);
    renderer.setZoomEnabled(true);
    renderer.setChartTitleTextSize(20);
    renderer.setDisplayValues(true);
    renderer.setShowLabels(true);
    SimpleSeriesRenderer r = renderer.getSeriesRendererAt(0);
    r.setGradientEnabled(true);
    r.setGradientStart(0, ColorUtil.BLUE);
    r.setGradientStop(0, ColorUtil.GREEN);
    r.setHighlighted(true);

    // Create the chart ... pass the values and renderer to the chart object.
    PieChart chart = new PieChart(buildCategoryDataset("Project budget", values), renderer);

    // Wrap the chart in a Component so we can add it to a form
    ChartComponent c = new ChartComponent(chart);

    // Create a form and show it.
    Form f = new Form("Budget");
    f.setLayout(new BorderLayout());
    f.addComponent(BorderLayout.CENTER, c);
    return f;

}

8.35.5. Animating a change in the data

Replacing a series' numbers and repainting redraws the chart in one jump. The com.codename1.charts.transitions package tweens the change instead: pick the transition that matches the model — XYSeriesTransition for one XYSeries, XYMultiSeriesTransition for a whole XYMultipleSeriesDataset, XYValueSeriesTransition for an XYValueSeries — and write the new numbers into its buffer rather than into the series itself.

XYSeriesTransition transition = new XYSeriesTransition(chart, readings);
transition.setEasing(SeriesTransition.EASING_IN_OUT);
transition.setDuration(600);

// The buffer starts out empty. Write the NEW shape of the series into
// it -- every point, not only the ones that changed -- because the
// buffer is what the series is tweened towards.
XYSeries next = transition.getBuffer();
next.add(0, 12);
next.add(1, 19);
next.add(2, 7);

transition.animateChart();

The buffer holds the shape you want to end up with, so put every point in it and not only the ones that moved: a point the buffer doesn’t mention keeps whatever the series already had, while a point the series doesn’t have yet grows in from zero. animateChart() registers the transition with the chart’s top-level container and drives it to that shape over setDuration(…​) milliseconds, easing as setEasing(…​) says — EASING_LINEAR, EASING_IN, EASING_OUT or EASING_IN_OUT. The buffer is drained when the animation finishes, so the same transition can be used again for the next update.

updateChart() applies the buffer the same way and repaints at once, with no animation, which is what you want for a change the user didn’t trigger.

XYSeriesTransition transition = new XYSeriesTransition(chart, readings);
transition.getBuffer().add(3, 22);
transition.updateChart();

8.36. Calendar

The Calendar class allows you to display a traditional calendar picker and optionally highlight days in various ways.

Prefer the Picker UI over the calendar for picking a date. It looks better on the devices.

Simple usage of the Calendar class looks something like this:

Form hi = new Form("Calendar", new BorderLayout());
Calendar cld = new Calendar();
cld.addActionListener((e) -> Log.p("You picked: " + new Date(cld.getSelectedDay())));
hi.add(BorderLayout.CENTER, cld);
hi.show();
The Calendar component
Figure 158. The Calendar component

8.37. ToastBar

The ToastBar class allows you to display none-obtrusive status messages to the user at the bottom of the screen. This is useful for such things as informing the user of a long-running task (like downloading a file in the background), or popping up an error message that doesn’t require a response from the user.

Simple usage of the ToastBar class looks something like this:

Status status = ToastBar.getInstance().createStatus();
status.setMessage("Downloading your file...");
status.show();

//  ... Later on when download completes
status.clear();
ToastBar with message
Figure 159. ToastBar with message

You can show a progress indicator in the ToastBar like this:

Status status = ToastBar.getInstance().createStatus();
status.setMessage("Hello world");
status.setShowProgressIndicator(true);
status.show();
Status Message with Progress Bar
Figure 160. Status Message with Progress Bar

You can automatically clear a status message/progress after a timeout using the setExpires method as such:

Status status = ToastBar.getInstance().createStatus();
status.setMessage("Hello world");
status.setExpires(3000);  // only show the status for 3 seconds, then have it automatically clear
status.show();

You can also delay the showing of the status message using showDelayed as such:

Status status = ToastBar.getInstance().createStatus();
status.setMessage("Hello world");
status.showDelayed(300); // Wait 300 ms to show the status

// ... Some time later, clear the status... This may be before it shows at all
status.clear();
ToastBar with a multiline message
Figure 161. ToastBar with a multiline message

8.37.1. Actions in ToastBar

Probably the best usage example for actions in toast is in the gmail style undo. If you aren’t a gmail user then the gmail app essentially never prompts for confirmation!

It does whatever you ask and pops a "toast message" with an option to undo. If you clicked by mistake you have 3-4 seconds to take that back.

This simple example shows you how you can undo any addition to the UI in a similar way to gmail:

Form hi = new Form("Undo", BoxLayout.y());
Button add = new Button("Add");

add.addActionListener(e -> {
    Label l = new Label("Added this");
    hi.add(l);
    hi.revalidate();
    ToastBar.showMessage("Added, click here to undo...", FontImage.MATERIAL_UNDO,
            ee -> {
                l.remove();
                hi.revalidate();
            });
});
hi.add(add);
hi.show();

8.38. SignatureComponent

The SignatureComponent provides a widget that allows users to draw their signature in the app.

Simple usage of the SignatureComponent class looks like:

Form hi = new Form("Signature Component");
hi.setLayout(new BoxLayout(BoxLayout.Y_AXIS));
hi.add("Enter Your Name:");
hi.add(new TextField());
hi.add("Signature:");
SignatureComponent sig = new SignatureComponent();
sig.addActionListener((evt)-> {
    System.out.println("The signature was changed");
    Image img = sig.getSignatureImage();
    // Now we can do whatever we want with the image of this signature.
});
hi.addComponent(sig);
hi.show();
The Signature Component
Figure 162. The signature Component

8.39. Accordion

The Accordion displays collapsible content panels.

Simple usage of the Accordion class looks like:

Form f = new Form("Accordion", new BoxLayout(BoxLayout.Y_AXIS));
f.setScrollableY(true);
Accordion accr = new Accordion();
accr.addContent("Item1", new SpanLabel("The quick brown fox jumps over the lazy dog\n"
        + "The quick brown fox jumps over the lazy dog"));
accr.addContent("Item2", new SpanLabel("The quick brown fox jumps over the lazy dog\n"
        + "The quick brown fox jumps over the lazy dog\n "
        + "The quick brown fox jumps over the lazy dog\n "
        + "The quick brown fox jumps over the lazy dog\n "
        + ""));

accr.addContent("Item3", BoxLayout.encloseY(new Label("Label"), new TextField(), new Button("Button"), new CheckBox("CheckBox")));

f.add(accr);
f.show();
The Accordion Component
Figure 163. The Accordion Component

8.40. Floating hint

FloatingHint wraps a text component with a special container that can animate the hint label into a title label when the text component is edited or has content within it:

Form hi = new Form("Floating Hint", BoxLayout.y());
TextField first = new TextField("", "First Field");
TextField second = new TextField("", "Second Field");
hi.add(new FloatingHint(first)).
        add(new FloatingHint(second)).
        add(new Button("Go"));
hi.show();
Floating hint
Figure 164. The hint floats on the field being edited and stays inside the one that isn’t

8.41. Floating Button

The material design floating action button is a powerful tool for promoting an action within your application.

FloatingActionButton is a round button that resides on top of the UI typically in the bottom right-hand side.
It has a drop shadow to distinguish it from the UI underneath and it can hide two or more actions under the surface. For example: you can create a simple single click button such as this:

FloatingActionButton fab = FloatingActionButton.createFAB(FontImage.MATERIAL_ADD);
fab.addActionListener(e -> ToastBar.showErrorMessage("Not implemented yet..."));
fab.bindFabToContainer(form.getContentPane());

Which will place a + sign button that will perform the action. Or you can create a nested action where a click on the button will produce a submenu for users to pick from for example:

FloatingActionButton fab = FloatingActionButton.createFAB(FontImage.MATERIAL_ADD);
fab.createSubFAB(FontImage.MATERIAL_PEOPLE, "");
fab.createSubFAB(FontImage.MATERIAL_IMPORT_CONTACTS, "");
fab.bindFabToContainer(form.getContentPane());
FloatingActionButton with submenu expanded
Figure 165. FloatingActionButton with submenu expanded

Those familiar with this widget know that there are many nuances to this UI that may be implemented/exposed in the future. The current API is intentionally simple and minimal for the common use cases, with the plan to refine it based on feedback.

8.41.1. Using floating Button as a badge

Floating buttons can also be used to badge an arbitrary component in the style popularized by iOS/macOS. A badge appears in the top right corner and includes special numeric details such as unread count..

The code below adds a simple badge to a chat button:

Form hi = new Form("Badge");

Button chat = new Button("Chat");
FontImage.setMaterialIcon(chat, FontImage.MATERIAL_CHAT, 7);

FloatingActionButton badge = FloatingActionButton.createBadge("33");
hi.add(badge.bindFabToContainer(chat, Component.RIGHT, Component.TOP));

TextField changeBadgeValue = new TextField("33");
changeBadgeValue.addDataChangedListener((i, ii) -> {
badge.setText(changeBadgeValue.getText());
badge.getParent().revalidate();
});
hi.add(changeBadgeValue);

hi.show();

The code above results in this, notice you can type into the text field to change the badge value:

Badge floating button in action
Figure 166. Badge floating button in action

8.42. SplitPane

The split pane component is a bit desktop specific but works reasonably well on devices. To get the image below you changed SalesDemo.java in the kitchen sink by changing this:

private Container encloseInMaximizableGrid(Component cmp1, Component cmp2) {
    GridLayout gl = new GridLayout(2, 1);
    Container grid = new Container(gl);
    gl.setHideZeroSized(true);

    grid.add(encloseInMaximize(grid, cmp1)).
            add(encloseInMaximize(grid, cmp2));
    return grid;
}

To:

private Container encloseInMaximizableGrid(Component cmp1, Component cmp2) {
    return new SplitPane(SplitPane.VERTICAL_SPLIT, cmp1, cmp2, "25%", "50%", "75%");
}
Split Pane in the Kitchen Sink Demo
Figure 167. Split Pane in the Kitchen Sink Demo

This is self-explanatory but "mostly." you have 5 arguments the first 3 make sense:

  • Split orientation

  • Components to split

The last 3 arguments seem weird but they also make sense once you understand them, they’re:

  • The least position of the split - 1/4 of available space

  • The default position of the split - middle of the screen

  • The most position of the split - 3/4 of available space

The units don’t have to be percentages they can be mm (millimeters) or px (pixels).

9. Component selector

The ComponentSelector class is a new class that brings the power of jQuery to Codename One. While it isn’t actually jQuery, it’s influenced by it. If you’re not familiar with jQuery, here is the 10 second intro.

jQuery is a JavaScript library, created by John Resig in 2006, that has become a staple of browser-based UI development. As of March 2017, over 70% of websites are using jQuery. The initial problem that jQuery solved was browser incompatibility issues. It provided a consistent API for most useful DOM methods so that the developer didn’t have to spend all their days and nights fighting with browser compatibility issues. In a way, it did for JavaScript, what Codename One does for mobile apps.

The other thing jQuery did was provide an elegant way to select and manipulate DOM elements (that is, HTML tags in a web page). It enabled developers to form sets of elements using a CSS-like syntax, and to operate on all elements of those sets, as if they were single elements, and they provided a fluent API to enable developers to chain multiple calls together, and delay onset of carpel tunnel for at least a few extra years.

With the new ComponentSelector class, you’ve adopted the following aspects of jQuery:

  1. CSS-like Selection Syntax - Support for a CSS-like syntax for "selecting" Components to be included in a set.

  2. Fluent API - The API is fluent, meaning you can chain multiple method calls together and reduce typing.

  3. Working on Sets - Methods of this class operate on sets of components as if they’re a single component. As you’ll see, this can be incredibly powerful.

  4. Effects - Support for common effects on components such as fadeIn(), fadeOut(), slideDown(), and slideUp().

9.1. Motivation by example

The following snippet creates a button, that, when pressed, will fade itself and all its sibling components out, and then fade them back in:

// ...

Button slideUp = $(new Button("Slide Up")) (1)
    .setIcon(FontImage.MATERIAL_EXPAND_LESS) (2)
    .addActionListener(e->{ (3)
        $(e) (4)
            .getParent() (5)
            .find(">*") (6)
            .slideUpAndWait(1000) (7)
            .slideDownAndWait(1000); (8)
    })
    .asComponent(Button.class); (9)
1- Creates a new Button and wraps it in a ComponentSelector so that you can use ComponentSelector’s fluent API to change the button.
2- Sets the icon on the button
3- Adds an action listener to the button
4- Wraps the source of the action event (which is the button) in a ComponentSelector so you can use fluent API.
5- Gets button’s parent component. (Actually it’s a set of components, size=1)
6- Gets all the direct children of the button’s parent container.
7- Hides all the children of the button’s parent (that is, the button and siblings) using a slide-up effect. Duration is 1000ms. Blocks until complete.
8- Shows the button and its siblings using a slide-down effect.
9- Returns the button itself to assign to the slideUp local variable.
For people like Shai who don’t like the jQuery $ syntax used for method names, you can or use the select() method as an alias for $().

Take a close look at that example, and imagine implementing that functionality the "old" way. It actually gets pretty hairy. That example should be enough to get you interested in using ComponentSelector.

Now look at some details of how it works and how to use it.

9.2. Selecting components

The first thing you’ll want to do with ComponentSelector, is to select the components that will be in your set. You can do this in many ways:

  1. You can use the CSS selector syntax. For example, $("Label, Button, #MyCalendar") = The set of all components on the current form with UIID=Label, or UIID=Button, or Name=MyCalendar.

  2. You can explicitly wrap one or more components. For example, $(myButton, myLabel, myCalendar) = A set with 3 components, myButton, myLabel, and myCalendar.

  3. You can change an existing set to create a new one. For example, $("Label").add("Button", true).add($("#MyCalendar")); = the same set formed in 1 above. (Labels, Buttons, and components with Name=MyCalendar)

9.2.1. Specifying the roots for queries

You can specify the roots for a query to limit the scope of the search. If you don’t provide a root, then the current form will be used as a root. If there is no current form yet, then a RuntimeException will be thrown. It’s always a good idea to provide a root if you’ve one.

For example, $(`Label, myContainer)` = The set of components with UIID="Label" inside the myContainer. This will scour the full subtree of myContainer for components that match the provided selector.

You can provide multiple roots as well. For example, $("Label", myContainer, anotherContainer) = The set of components with UIID="Label" inside either myContainer or anotherContainer.

You could also provide set of components, and by extension a ContainerSelector (since it implements java.util.Set<Component>) as the roots. For example, $("CalendarDay", $("Calendar")) = The set of all components with UIID "CalendarDay" inside containers with UIID=Calendar.

Finally, ComponentSelector provides a find() method that implicitly uses the current set as the roots. For example, $("Calendar").find("CalendarDay") is equivalent to $("CalendarDay", $("Calendar")).

9.2.2. Selector syntax

Until now the selector syntax has only been described as like CSS. It’s like CSS, but it differs where Codename One’s component model differs from the JavaScript DOM. Rather than a formal grammar, the examples below show what the syntax can express.

  1. $("Label") - The set of all components on the form with UIID "Label"

  2. $("#MyField") - The set of all components on the form with Name MyField.

  3. $(".some-tag") - The set of all components on the form with the tag "some-tag." (You’ll discuss tags below).

  4. $("Label#MyField") - The set of all components on the form with both UIID=Label and Name=MyField.

  5. $("Label#MyField.some-tag") - The set of all components on the form with UIID=Label, Name=MyField, and tag "some-tag."

  6. $("*") - The set of all components on the form.

  7. $(".some-tag.some-other-tag") - All components with both tags "some-tag" and "some-other-tag"

  8. $("#MyContainer *") - All components contained in the subtree whose root has Name=MyContainer. The "MyContainer" wouldn’t be included in this set. Only its descendants.

  9. $("#MyContainer > *") - Direct children of components with Name=MyContainer. (The "MyContainer" component would not be included in this set. Only its children.)

  10. $("#MyContainer > * > Label") - Direct grandchildren of components with Name=MyContainer. Only grand children with UIID="Label" are included.

9.2.3. Tags

Some examples above mention the use of "tags." Tags are analogous to "classes" in CSS. The goal was to provide the same kind of functionality as CSS classes, but in Java the term "class" is a little busy, so the term "tag" is used instead. You can add as many tags to a component as you like. You can then use those tags to assist in your queries. Tags are added using the addTags() method, and they’re removed using the removeTags() method.

Here is an example that uses tags to implement table striping so that even rows are a different color than odd rows in a table:

TableLayout tl = new TableLayout(numRows, numCols);
Container table = new Container(tl);
int rowNum = 0;
for (String[] row : data) {
    int colNum = 0;
    for (String cell : row) {
        table.add(
            tl.createConstraint(rowNum, colNum),
            $(new Button(cell))
                .setUIID("Label")
                .addTags(rowNum % 2 == 0 ? "even" : "odd")
                .asComponent()
        );
        colNum++;
    }
    rowNum++;
}
$(".even", table)
    .setBgColor(0xcccccc)
    .setBgTransparency(255);
Table striping screenshot

In the above example you add a tag to each label when you add it to the table or either "even" or "odd." You then use the "even" in your ComponentSelector to set the background color and transparency of all labels in even rows. In this example, you actually build the table and set the striping in the same place, but consider the situation when the table is constructed by another API. This approach would allow the API to provide more extensibility by tagging the elements and allowing the library user to decide how to style the even and odd rows.

9.3. Modifying styles

ComponentSelector includes wrappers for most of the methods of com.codename1.ui.plaf.Style so that you can change the styles of selected components using a fluent API. This was demonstrated a little bit in the table striping example (setBgColor() and setBgTransparency() were called on even rows of the table). It’s worth going over this in a little more detail though as Codename One styles have "state." For example, For the snippet of code:

$(".even", table)
    .setBgColor(0xcccccc)
    .setBgTransparency(255);

Were you setting these values on the "selected" style, the "unselected" style, the "disabled" style, "ALL" styles? In fact you were setting these values on the "current" style. That is, the call to setBgColor() caused something like the following loop to happen on all the components in the set:

for (Component c : evenComponents) {
    c.getStyle().setBgColor(0xcccccc);
}

If a component was in "selected" state, then this would change the selected style. If it was in pressed state, then it would change the "pressed" style. Etc. What if you wanted to specifically change the styles in the "pressed" state. Then you would call selectPressedStyle() prior calling your style mutation methods. For example:

$(".even", table)
    .selectPressedStyle()
    .setBgColor(0xcccccc)
    .setBgTransparency(255);

Or, ComponentSelector supports a "state" pseudo-selector that will initialize the selected style so that you don’t have to call selectXXXStyle() before making changes. For example, The following snippet is equivalent to the previous:

$(".even:pressed", table)
    .setBgColor(0xcccccc)
    .setBgTransparency(255);

The following "state" pseudo-selectors are available:

  1. :pressed - Selects the "pressed" style

  2. :selected - Selects the "selected" style

  3. :unselected - Selects the "unselected" style

  4. :disabled - Selects the "disabled" style

  5. :all - Selects the ALLStyles proxy for each component (so that you can target all styles together)

  6. :* - Same as :all

If you want to access Style objects directly, you can use one of:

  1. getPressedStyle() - Returns a proxy style with all the pressed styles of components in the set.

  2. getSelectedStyle()

  3. getUnselectedStyle()

  4. getDisabledStyle()

  5. getAllStyles()

9.4. Effects and transitions

As mentioned above, and demonstrated in the first example of this article, ComponentSelector provides a simple way to add eye-candy to your apps. It implements the fadeIn(), fadeOut(), slideUp(), and slideDown() methods and their AndWait() derivations for showing and hiding components in your UI with style. You no longer have any excuse not to use a transition for showing and hiding elements in your UIs. Using a transition always feels nicer to the user.

Also to these basic effects, ComponentSelector wraps all the existing animation functionality of the Component and Container classes but with proper accommodations for animating multiple elements simultaneously.

For example, Consider this example, that shows a button that replaces itself and all siblings with replacement labels, and then replaces them back:

Button replace = $(new Button("Replace Fade/Slide"))
    .setIcon(FontImage.MATERIAL_REDEEM)
    .addActionListener(e->{
        $(e).getParent()
            .find(">*")  (1)
            .replaceAndWait(c->{ (2)
                return $(new Label("Replacement")) (3)
                    .putClientProperty("origComponent", c) (4)
                    .asComponent();
            }, CommonTransitions.createFade(1000)) (5)
            .replaceAndWait(c->{
                Component orig = (Component)c.getClientProperty("origComponent");
                if (orig != null) {
                    c.putClientProperty("origComponent", null);
                    return orig; (6)
                }
                return c;
            }, CommonTransitions.createCover(CommonTransitions.SLIDE_HORIZONTAL, false, 1000)); (7)
    })
    .asComponent(Button.class);
1Finds all siblings of the source button
2Call replaceAndWait() with a mapping function to define the component that should replace each component. This will replace each component in the set with a replacement component in its respective container. This will also return a new ComponentSelector with the set of replacement components.
3In your "mapper" callback, you will return a new Label component to replace each existing component.
4You store the original component in the replacement’s client properties so that you can swap it back afterwards.
5You use the "Fade" transition for the replacement.
6In the second replaceAndWait() call’s mapper method, you return the original component which you retrieved from the replacement’s client properties.
7You use a cover transition for the replacement.

9.5. Component method wrappers

Most mutator methods in Component and Container include a corresponding wrapper method in ComponentSelector. Some more common component subclasses have corresponding wrappers as well. For example, addActionListener(event) will add the event to all Buttons in the set. setText(txt) will set the text on all labels, text areas, and buttons. If there are other commonly used methods that you would like to see included in ComponentSelector, file an issue, but you should find the current state to be comprehensive.

9.5.1. Tree navigation

Component and Container include a few tree navigation methods. For example, Component.getParent() returns the parent container of the current component. In cases like this, where ComponentSelector wraps such a method, it will return a set of corresponding components resulting from calling those methods on the components in the current set.

For example, $(comp1, comp2).getParent() will include both the parent of comp1 and comp2 (and if they have the same parent, then the set will include a single element.

This can get interesting when you start combining these methods. For example, $(comp1, comp2).getParent().getComponentAt(10) will be the set of components that are the 11th children of comp1 and `comp2’s parents. If neither of their parents have that many children, then it will be an empty set. If one of them has that many, then it will be a set of size 1.

9.6. More demos

A demo app exercises several of the features covered here. Most of the interesting code is in StyleBuilderDemo.java.

A short screencast of the demo app is on YouTube:

10. Rich Text and Code Editing

Codename One includes two full-document editors. Use RichTextArea when users compose formatted prose and the application stores HTML. Use CodeEditor for source code, configuration files, queries, or any text that benefits from syntax highlighting, line numbers, completion, and diagnostics. Both are lightweight Codename One components and share the same editing, selection, scrolling, undo, and platform-input infrastructure. When you only need to display formatted text — no editing — use RichTextComponent, the read-only, word-wrapping renderer described later in this chapter; it reuses the same document model and styling engine.

RichTextArea and CodeEditor rendered together
Figure 168. The rich-text and code editors rendered by the same lightweight engine

10.1. Editor architecture and backend selection

Both components extend AbstractEditorComponent. The public components send semantic commands and queries to one of two backends:

  • A port can provide a native peer through CodenameOneImplementation.createNativeEditorPeer(…​).

  • Otherwise the components use the pure Codename One engine in com.codename1.ui.editor. It paints text, selections, carets, gutters, diagnostics, and completion UI with Graphics and Font, and it drives keyboard and IME input through the low-level text-input contract.

Backend selection is transparent to application code. isNativeEditor() reports the native-peer case. The pure engine is the default renderer on every platform; there is no browser fallback.

Editor initialization can be asynchronous for a native peer. Commands issued immediately after construction are queued. Use onReady(Runnable) or a ready listener when work must run only after the backend exists. Reads use callbacks (getHtml(…​), getText(…​), and getCursorPosition(…​)) so the same application code works with either backend.

10.1.1. The editor package

The com.codename1.ui.editor package separates editing mechanics from feature-specific rendering. EditorDocument owns normalized UTF-16 text and line offsets. EditorView owns the caret, selection, composition range, scrolling, hit testing, clipboard commands, and undo transactions. CodeView and RichView extend that surface instead of creating independent input implementations.

The code layer adds incremental tokenization, palettes, completion, diagnostics, indentation, and the line-number gutter. The rich layer keeps InlineStyles, RichBlocks, hyperlink targets, and image runs parallel to the text. Every mutation, including undo and redo, updates those parallel models in the same transaction. HtmlImporter and HtmlSerializer are model adapters; they don’t implement a second HTML tokenizer. Fragment parsing and entity decoding go through the framework’s tolerant HTMLParser, which extends XMLParser. The parser’s fragment mode retains modern attributes such as data-* while the legacy HTMLComponent path keeps its existing validation behavior.

EditorHost is the narrow bridge between a view and its owner. It starts and stops platform text input, pushes updated editing state to the port, and reports semantic editor events. This split lets the same document and interaction engine run inside the full editors and inside the lightweight EditField component.

10.2. Focus, change events, and editability

focusEditor() moves focus into the editing surface and opens the virtual keyboard when appropriate. blurEditor() relinquishes focus and dismisses the active platform input session. setEditable(false) keeps the document visible while preventing edits; CodeEditor.setReadOnly(true) is the code-oriented equivalent.

Register an addChangeListener(…​) listener to observe user edits and programmatic mutations. Avoid saving on every keystroke when serialization or storage is expensive; debounce the listener, then read the document through the asynchronous callback.

10.3. Platform text input and input methods

The pure editor doesn’t treat a key release as text. Modern input can come from a virtual keyboard, autocorrect, dictation, handwriting, a hardware keyboard, or an input method editor (IME). The platform therefore drives a TextInputClient with semantic operations:

  • commitText(…​) inserts final text, including accepted autocorrect and dictation results.

  • setComposingText(…​) replaces the current marked-text range while an IME is still composing. finishComposing() finalizes that range.

  • deleteSurroundingText(…​) implements soft-keyboard deletion without manufacturing key codes.

  • onKeyCommand(…​) carries navigation, selection, clipboard, undo, and redo commands.

  • Geometry methods map UTF-16 offsets to caret and selection rectangles so native candidate windows, selection handles, and accessibility UI align with the lightweight rendering.

TextInputConfig tells the port which keyboard constraint, return-key action, autocorrection, capitalization, and multiline behavior the client needs. Rich prose enables normal language assistance; source editing disables autocorrection and automatic capitalization. The immutable TextInputState returns the authoritative text, selection, and composing range to the port after each edit.

All offsets in this contract are UTF-16 indices, matching Java String, Android Editable, and Apple string indices. The document canonicalizes CRLF and bare CR line endings to LF before it updates the caret, undo history, formatting runs, or platform state. Port authors must deliver composition updates on the EDT and must stop the input session when the client loses focus.

TextInputClient, TextInputConfig, and TextInputState are general port-level APIs. Custom lightweight editing components can implement the same protocol, but ordinary applications normally use RichTextArea or CodeEditor and don’t bind an input client directly.

10.4. Lightweight fields with EditField

TextArea and TextField keep their established native editing behavior. When an application wants Codename One to retain control of painting, selection, scrolling, focus, and clipboard handling for a plain field, use EditField. It offers an API familiar from TextField — hint text, constraint, single-line or multiline mode, rows and columns, data-change listeners, and action events — but renders and edits entirely through the pure com.codename1.ui.editor engine, with no native peer. Adoption is opt-in per component; existing TextField and TextArea usages are unaffected.

EditField title = new EditField("", "Title", TextArea.ANY);
title.setSingleLineTextArea(true);
title.setColumns(80);

EditField notes = new EditField();
notes.setSingleLineTextArea(false);
notes.setRows(5);
notes.setColumns(30);

EditField extends EditorView directly, so selection, caret navigation, arrow-key movement, undo, and clipboard commands are the same mechanics the full editors use. A single-line EditField fires its action event on the return key and strips embedded newlines; a multiline EditField grows to its configured row count. Because there is no native overlay, the field behaves identically on every platform that implements the low-level text-input contract.

10.5. Selection, bidirectional text, and scrolling

On touch devices, a tap places the caret, a long press selects a word, and draggable handles adjust the selection. The loupe follows a handle or long-press placement gesture. A normal one-finger drag scrolls the document and doesn’t turn the swipe into a selection. On desktop, mouse drag selects text, double-click selects a word, triple-click selects a line, and the mouse wheel or trackpad scrolls the document. Standard command/control shortcuts handle copy, cut, paste, select all, undo, and redo.

Hebrew, Arabic, and mixed left-to-right/right-to-left lines use Unicode bidirectional layout. Glyph painting, hit testing, caret movement, and selection rectangles share the same visual runs. Set the component’s RTL flag to choose the paragraph base direction.

10.6. RichTextArea

RichTextArea accepts and returns an HTML fragment. The importer maps common paragraph, heading, blockquote, preformatted, list, link, image, bold, italic, underline, strike-through, color, highlight, and relative-size markup into a document model. getHtml(…​) emits canonical HTML from that model; it preserves meaning rather than the input’s exact whitespace, tag aliases, or attribute order.

Form hi = new Form("Compose", new BorderLayout());
RichTextArea editor = new RichTextArea();
editor.setPlaceholder("Write something...");
editor.setHtml("<h2>Trip itinerary</h2><p>Meet at the <b>main lobby</b>.</p>");

Toolbar tb = hi.getToolbar();
tb.addCommandToRightBar("B", null, e -> editor.bold());
tb.addCommandToRightBar("I", null, e -> editor.italic());
tb.addCommandToRightBar("List", null, e -> editor.insertUnorderedList());
tb.addCommandToRightBar("Save", null, e ->
        editor.getHtml(html -> Log.p("User wrote: " + html)));

hi.add(BorderLayout.CENTER, editor);
hi.show();
RichTextArea with formatted itinerary content
Figure 169. RichTextArea rendering headings, inline formatting, and a list

The formatting methods act on the current selection: bold(), italic(), underline(), strikeThrough(), ordered and unordered lists, indentation, alignment, foreground and highlight colors, block formats, and font sizes. queryCommandState(…​) keeps toolbar toggles synchronized with the selection. removeFormat() clears inline formatting; undo() and redo() restore text together with inline styles, paragraph attributes, links, and image runs.

Use setHtml(…​) to replace the document and insertHtml(…​) to insert a fragment at the selection. Fragment insertion retains its formatting and block structure. createLink(url) stores the target in the model, and removeLink() removes it. insertImage(url) accepts normal URLs, data URIs, and supported application resources; the source is retained so getHtml(…​) can emit an img element when the document is saved and reloaded.

10.6.1. HTML, RTF, Markdown, and AsciiDoc

setContent(…​) and insertContent(…​) accept a RichTextFormat. HTML is parsed directly with the shared HTMLParser. Markdown and AsciiDoc use their own bidirectional model adapters; they never pass through HTML. Headings, emphasis, inline code, links, images, lists, quotes, and literal blocks are mapped directly to the rich model and serialized directly back with getMarkdown(…​) or getAsciiDoc(…​). The RTF importer likewise maps paragraph breaks, Unicode escapes, bold, italic, underline, strike-through, and skips metadata destinations such as font and color tables.

Editing and reading the same format returns canonical output for that format. Requesting a different output format performs an explicit model conversion, just as getHtml(…​) canonicalizes a visual rich-text document.

editor.setContent("# Release notes\n\nThis is **ready**.", RichTextFormat.MARKDOWN);
editor.insertContent("{\\rtf1\\ansi {\\i pasted notes}}", RichTextFormat.RTF);
editor.insertContent("== Details\n\n* Portable\n* Lightweight", RichTextFormat.ASCIIDOC);

Rich paste uses ClipboardContent, a MIME-keyed container exposed by Display.copyToClipboard(…​) and Display.getClipboardContent(). RichTextClipboardData is its convenience subtype for text/plain, text/html, text/rtf, text/markdown, and text/asciidoc. A writer publishes all representations in one operation and a reader selects the richest format it understands; plain EditorView consumers use text/plain. Copying a rich selection publishes canonical HTML, RTF, Markdown, AsciiDoc, and the required plain fallback. Java SE publishes native DataFlavor values, Android publishes native plain and HTML clip data, the JavaScript port uses ClipboardItem when the browser allows it, and iOS publishes pasteboard types. A port that only has a text clipboard still publishes plain text externally and retains the complete ClipboardContent for in-application use.

The negotiation API belongs at the implementation boundary, not inside RichTextArea. Port authors override clipboard handling in CodenameOneImplementation and return the formats actually present on the system clipboard. Applications can use the same API for domain-specific formats without changing the editor.

RTF is also recognized when a clipboard provider returns it as a string beginning with an RTF header. Format import is intentionally a practical interchange subset, not a layout-compatible word processor: unsupported RTF destinations and advanced Markdown or AsciiDoc extensions degrade to the supported text and structure rather than controlling document layout.

Treat imported HTML as document data, not as a browser page. The pure importer doesn’t execute scripts. Keep image URLs and link destinations subject to the same trust and validation rules used by the rest of the application.

10.7. Read-only rich text with RichTextComponent

RichTextComponent (in com.codename1.ui) is the read-only counterpart to RichTextArea. It renders multi-styled, word-wrapped text — headings, bold, italic, underline, strike-through, inline code, colored and highlighted spans, per-paragraph alignment and indentation, ordered and unordered lists, block quotes, preformatted blocks, inline images, and tappable hyperlinks — without any of the editing machinery. It sits between a single-style SpanLabel (one style for the whole component) and a full BrowserComponent (a native web view): a lightweight native component that keeps a distinct style per character, measures and paints text directly, and reports an accurate height-for-width preferred size, so it embeds cleanly inside ordinary layouts. It’s the modern replacement for the deprecated HTMLComponent.

It shares the editor’s document model and the RichRunPainter styling primitive, so styled text rendered read-only matches what the editor produces for the same content.

Content is supplied in any of the interchange formats the rich model understands, or built up programmatically from styled runs:

RichTextComponent view = new RichTextComponent();

// From Markdown, HTML, or any RichTextFormat:
view.setMarkdown("# Trip summary\n\n"
        + "Departs **09:40**, arrives *11:15*. See the [itinerary](app://itinerary) for details.\n\n"
        + "- Window seat\n"
        + "- Carry-on only");
// view.setHtml("<h1>Trip summary</h1><p>Departs <b>09:40</b> ...</p>");
// view.setContent(source, RichTextFormat.ASCIIDOC);

// Or assembled run by run with editor TextStyle values:
RichTextComponent built = new RichTextComponent();
built.append("Status: ", TextStyle.DEFAULT)
        .append("confirmed", TextStyle.DEFAULT.withBold(true).withForeColor(0x1a7f37));

form.add(view);

Hyperlinks in the content fire an ActionListener whose event source is the link target string, so the application decides what a link means:

view.addLinkListener(e -> Display.getInstance().execute((String) e.getSource()));

The component resolves inline images through an ImageResolver, keeping image loading and caching policy in the application rather than the component. Return null to render a placeholder:

view.setImageResolver(src -> myImageCache.get(src));

By default (SizeMode.SHRINK) the component is exactly as tall as its wrapped content at the width it is given, which is ideal inside a scrollable Form. SizeMode.SCROLL instead keeps the size the parent assigns and scrolls the content vertically. setTextAlign(…​) overrides the alignment of every paragraph, and preferredSizeForWidth(width) is a stateless height-for-width query for custom layout managers.

The same trust guidance applies as for RichTextArea — imported HTML is document data, not a browser page; validate link targets and image sources with the application’s own rules.

10.8. CodeEditor

CodeEditor provides incremental highlighting for java, kotlin, javascript, python, css, xml, json, and c. It includes a line-number gutter, light and dark palettes, automatic bracket and quote closing, tab expansion, automatic indentation, horizontal and vertical scrolling, and read-only viewing.

Form hi = new Form("Editor", new BorderLayout());
CodeEditor editor = new CodeEditor();
editor.setLanguage("java");
editor.setTheme("light");          // or "dark"
editor.setShowLineNumbers(true);
editor.setText("public class Main {\n\n}");
hi.add(BorderLayout.CENTER, editor);
hi.show();
CodeEditor showing highlighted Java source
Figure 170. CodeEditor with Java syntax highlighting and line numbers

Choose the language with setLanguage(…​), select "light" or "dark" with setTheme(…​), toggle the gutter with setShowLineNumbers(…​), and configure indentation with setTabSize(…​). insertAtCursor(…​) replaces the active selection, while getCursorPosition(…​) reports the current UTF-16 caret offset asynchronously.

The Codename One Playground uses CodeEditor directly for its Java and CSS source panes. This uses the same language highlighting, line-number gutter, keyboard navigation, diagnostics, and diagnostic tooltips available to applications. Compiler diagnostics are passed to setDiagnostics(…​) with their source ranges; status messages use lightweight Codename One components beneath the editor. The Playground doesn’t install or bundle a separate editor engine. Source editing disables platform spelling and autocorrection; the only wavy underlines the editor draws are diagnostics supplied through setDiagnostics(…​).

10.8.1. Custom syntax highlighters

Built-in languages use grammar-specific incremental lexical analyzers. JSON doesn’t accept single-quoted strings, CSS doesn’t interpret // as a comment, XML tracks tags and attributes, Python carries triple-quoted strings across lines, and C-family numeric prefixes are validated by radix. These rules are lexical by design; a compiler or language server remains the authority for diagnostics.

Third-party libraries can register a SyntaxHighlighter under any language ID. The highlighter receives one line and the state returned for the preceding line, then returns ordered token spans and the next state. Tokens can use the built-in semantic kinds or provide explicit light and dark RGB colors. Registration is global and the pure renderer invokes the Java highlighter directly, so a custom language needs no platform-specific grammar.

SyntaxHighlighter properties = (line, state) -> {
    List<SyntaxToken> tokens = new ArrayList<>();
    int equals = line.indexOf('=');
    if (equals > 0) {
        tokens.add(new SyntaxToken(0, equals, SyntaxToken.PROPERTY,
                0x005cc5, 0x9cdcfe));
    }
    return new SyntaxHighlightResult(tokens, 0);
};
CodeEditor.registerSyntaxHighlighter("properties", properties);

CodeEditor editor = new CodeEditor("properties", "accent=#0a66c2");

10.8.2. Asynchronous code completion

A CodeCompletionProvider receives the complete source and caret offset, then delivers proposals through its callback. The callback can run after a local analysis or network/language-server request; the editor doesn’t block the EDT while proposals are produced.

editor.setCompletionProvider((sourceEditor, code, cursor, results) -> {
    List<CodeCompletion> proposals = new ArrayList<>();
    proposals.add(new CodeCompletion("println(String value)", "println()")
            .setType("method")
            .setDetail("void"));
    proposals.add(new CodeCompletion("private").setType("keyword"));
    results.onSucess(proposals);
});

Return an empty list or null to dismiss the popup. A proposal can use different display and insertion text, plus a type badge and detail string.

10.8.3. Diagnostics

setDiagnostics(…​) accepts 1-based source ranges. Errors, warnings, and informational entries appear under only the supplied character range, with a gutter marker for the affected line. Hover the range or marker on desktop, or tap it on touch devices, to display its message.

editor.setDiagnostics(Arrays.asList(
        new CodeDiagnostic(6, 9, 6, 16, "Cannot resolve method prinln")
                .setSeverity(CodeDiagnostic.ERROR),
        new CodeDiagnostic(3, 5, "Value is never read")
                .setSeverity(CodeDiagnostic.WARNING)
));

Replace the list whenever a compiler, linter, or language server publishes a newer analysis. Passing an empty list clears the current diagnostics.

10.9. Port integration checklist

The pure engine drives the editors on every platform, so a port must implement the low-level text-input contract: isTextInputSupported(), startTextInput(…​), updateTextInputState(…​), and stopTextInput(…​) in CodenameOneImplementation. The binding must distinguish committed from composing text, preserve UTF-16 offsets, map native navigation and clipboard actions to TextInputClient.KEY_*, update native selection state after programmatic edits, and anchor candidate UI with the supplied caret geometry.

A port with a better platform editor can instead supply a native peer and implement the existing semantic command/query channel. In both cases application code, change listeners, ready handling, and asynchronous queries remain the same.

11. Accessibility Semantics

Codename One components are lightweight: they’re painted by Codename One instead of being represented by a native widget on every platform. The accessibility semantics API builds an explicit, immutable virtual tree from those components and presents it to VoiceOver, TalkBack, Windows UI Automation, AT-SPI, Java Access Bridge, and web accessibility APIs.

The semantic tree is separate from the visual tree. This is important for renderer-backed controls, merged cards, decorative content, custom controls, and applications that need a reading order different from paint order.

11.1. Built-in semantics

Standard components work without extra configuration. Button, CheckBox, RadioButton, Slider, text fields, lists, tables, tabs, labels, dialogs, and containers infer appropriate roles, values, states, collection metadata, and standard actions. Renderer-backed List rows are exposed as stable virtual children even though no Component exists for each painted row.

Component.setAccessibilityText() remains supported. It’s a compatibility alias for the semantic label. New code should use getSemantics() when it needs more than a label.

Button save = new Button("Save");
save.getSemantics()
        .setHint("Saves the edited profile")
        .setIdentifier("profile-save");

Changes to text, selection, focus, enabled state, bounds, scrolling, ranges, tabs, and the current Form automatically invalidate the native semantic tree. Custom components should call accessibilityChanged() after a semantic value changes outside a standard setter.

11.2. Roles, names, values, and state

AccessibilityRole contains portable control roles including buttons, toggle buttons, switches, checkboxes, radio buttons, headings, links, images, text and search fields, sliders, progress indicators, lists, grids, rows, cells, headers, tabs, dialogs, alerts, menus, toolbars, combo boxes, trees, and separators. A port maps an unsupported role to the closest native role without discarding its label or state.

wifiSwitch.getSemantics()
        .setRole(AccessibilityRole.SWITCH)
        .setLabel("Wi-Fi")
        .setChecked(AccessibilityCheckedState.CHECKED)
        .setEnabled(Boolean.TRUE)
        .setHint("Double tap to turn Wi-Fi off");

title.getSemantics() .setHeadingLevel(2) .setIdentifier("network-heading");

Tri-state controls use UNCHECKED, CHECKED, or MIXED. Nullable Boolean properties distinguish an explicit state from an unspecified state. Available properties include selected, expanded, enabled, invalid, busy, read-only, required, multiline, obscured, pressed, and current. setValidationError() supplies both the error message and invalid state. setPaneTitle() describes a newly displayed pane or screen, and setModal(true) identifies a modal scope.

Use setRoleDescription() only when the platform role isn’t sufficiently specific. Localize labels, hints, descriptions, errors, pane titles, role descriptions, and custom action labels.

11.3. Ranges and editable values

AccessibilityRange describes minimum, maximum, current value, increment, and an optional spoken value. The spoken value is useful for ranges such as ratings and durations where a raw number is ambiguous.

rating.getSemantics()
        .setRole(AccessibilityRole.SLIDER)
        .setLabel("Rating")
        .setRange(new AccessibilityRange(0, 5, 3, 1, "3 out of 5"));

Editable controls can provide SET_VALUE or SET_TEXT actions. Sliders and other adjustable controls should expose INCREMENT and DECREMENT. Standard components infer these actions.

11.4. Custom accessibility actions

An AccessibilityAction has a stable ID, a localized label, an enabled state, and a handler. Handlers always execute on the Codename One EDT, including when the action originates on a native accessibility thread.

card.getSemantics().addAction(new AccessibilityAction(
        "archive",
        "Archive message",
        new AccessibilityAction.Handler() {
            public boolean perform(Component component, Object argument) {
                archiveMessage();
                return true;
            }
        }
));

Use the standard IDs in AccessibilityAction for activation, long press, increment, decrement, set value, set text, selection, character or word cursor movement, focus, show-on-screen, dismiss, expand, collapse, scrolling, and clipboard operations. A custom ID must be stable and its label must not be null.

11.5. Grouping and virtual descendants

AccessibilityGrouping controls how a component participates in the semantic tree:

  • AUTO uses normal component inference.

  • LEAF exposes the component but none of its descendants.

  • GROUP exposes the component as a semantic container and retains its descendants.

  • MERGE_DESCENDANTS combines descendant labels, descriptions, values, state, and actions into one node.

  • EXCLUDE removes the component but promotes its descendants to the semantic parent.

  • EXCLUDE_SUBTREE removes the component and all descendants.

summaryCard.getSemantics()
        .setRole(AccessibilityRole.GENERIC)
        .setGrouping(AccessibilityGrouping.MERGE_DESCENDANTS);

For content that’s painted without child components, attach an AccessibilityChildProvider, or add standalone AccessibilityNode children. Every virtual node must use a stable virtualKey. Stable keys preserve native focus while rows are updated or recycled.

chart.getSemantics().setChildProvider(new AccessibilityChildProvider() {
    public List<AccessibilityNode> getAccessibilityChildren(Component owner) {
        List<AccessibilityNode> result = new ArrayList<AccessibilityNode>();
        for (ChartPoint point : points) {
            AccessibilityNode node = new AccessibilityNode("point-" + point.getId());
            node.setRole(AccessibilityRole.IMAGE)
                    .setLabel(point.getLabel())
                    .setValue(point.getFormattedValue())
                    .setBounds(point.getBounds());
            result.add(node);
        }
        return result;
    }
});

Virtual bounds are relative to the owning component. Component nodes use their clipped absolute bounds.

11.6. Traversal order

Visual order is the default reading order. setSortKey() assigns an ordered semantic key among siblings. setTraversalBefore() and setTraversalAfter() express a direct relationship when a numeric order would be fragile.

cancel.getSemantics().setSortKey(1);
continueButton.getSemantics().setSortKey(2);
help.getSemantics().setTraversalAfter(continueButton);

Keep traversal relationships within the same semantic parent. The tree builder resolves these constraints deterministically and preserves stable node IDs.

11.7. Live regions and screen changes

Set AccessibilityLiveRegion.POLITE for non-urgent updates and ASSERTIVE for errors or urgent status. OFF is the default. Live changes produce the corresponding platform notification or ARIA live update.

status.getSemantics()
        .setRole(AccessibilityRole.ALERT)
        .setLiveRegion(AccessibilityLiveRegion.POLITE);
status.setText("Upload complete");

Form changes and nodes with a pane title produce screen or pane transition notifications. Display.announceForAccessibility() remains available for an exceptional announcement that isn’t represented by persistent UI. Prefer a live region for state that’s visible on screen.

11.8. User accessibility preferences

Semantics describe what a control means. Accessibility preferences describe how the user wants the interface presented. Read these preferences through Display or the matching static shortcuts in CN:

APIMeaning

isHighContrastEnabled()

The user requests stronger foreground and background contrast.

isDifferentiateWithoutColorEnabled()

The user requests labels, shapes, or patterns in addition to color.

getColorVisionDeficiency()

Returns NONE, PROTANOPIA, DEUTERANOPIA, TRITANOPIA, MONOCHROMACY, or UNKNOWN.

isReduceMotionEnabled()

Nonessential motion should be removed or replaced with an instant or cross-fade change.

isReduceTransparencyEnabled()

Blur and translucent surfaces should use an opaque alternative.

isBoldTextEnabled()

The system requests a heavier text weight.

isInvertColorsEnabled() and isGrayscaleEnabled()

The operating system is transforming the displayed colors.

isOnOffSwitchLabelsEnabled()

Switches should include a visible on/off distinction.

isScreenReaderEnabled()

A screen reader or touch-exploration service is active.

An unavailable preference returns false; an unavailable color-vision mode returns UNKNOWN. Don’t hide accessibility support behind isScreenReaderEnabled(). A screen reader can start after the application launches, and other assistive technologies consume the same semantic tree.

if (CN.isHighContrastEnabled()) {
    animatedChart.setUIID("HighContrastChart");
}

// Important state always uses text or an icon as well as color. The
// preference is an extra signal, not permission to use color alone.
if (CN.isDifferentiateWithoutColorEnabled()
        || CN.getColorVisionDeficiency() != AccessibilityColorVisionDeficiency.NONE) {
    connectionStatus.setText("Disconnected: action required");
}

if (CN.isReduceMotionEnabled()) {
    animatedChart.putClientProperty("animate", Boolean.FALSE);
}
if (CN.isReduceTransparencyEnabled()) {
    animatedChart.setUIID("OpaqueChart");
}
Never use getColorVisionDeficiency() == NONE as permission to convey information only through color. The setting can be unavailable, a person might not enable a system filter, and contrast can change with lighting and display hardware. Design the default interface with redundant cues, then use preferences for further adaptation.

Preference detection uses each platform’s public signals. iOS and macOS expose contrast, differentiate-without-color, motion, transparency, bold text, inversion, grayscale, switch labels, and VoiceOver; Apple doesn’t expose the selected color-filter type, so the deficiency value is UNKNOWN. Android reads high-text contrast, display inversion and color-correction mode, animation scale, font-weight adjustment, and touch exploration. Windows reads High Contrast, client-area animation, and screen-reader settings. Linux detects the GTK HighContrast theme, disabled GTK animation, and ATK bridge activation. JavaScript uses forced-colors, prefers-contrast, prefers-reduced-motion, and prefers-reduced-transparency. The Java SE simulator provides deterministic controls for every portable signal.

11.9. Collections

AccessibilityCollectionInfo describes row and column counts, hierarchy, and selection mode. AccessibilityCollectionItemInfo describes row and column index, spans, position and size in a set, nesting level, and header status.

grid.getSemantics().setCollectionInfo(
        new AccessibilityCollectionInfo(20, 3, false,
                AccessibilityCollectionInfo.SELECTION_MULTIPLE));

cell.getSemantics().setCollectionItemInfo( new AccessibilityCollectionItemInfo( 4, 1, 2, 1, 5, 20, 1, false));

Row and column indexes are zero-based. Position and set size are one-based; use -1 when unknown. Lists, tables, tabs, and their items infer this metadata where possible.

Renderer-backed lists keep the full model size in their collection metadata but materialize only the visible rows, a small navigation buffer, and the selected row as virtual children. Forward and backward semantic scroll actions move the window. This keeps accessibility-tree work proportional to the viewport instead of invoking a cell renderer for every row whenever a large list scrolls.

11.10. Inspecting and testing semantics

AccessibilityInspector returns an immutable snapshot. A snapshot is safe to inspect without racing native accessibility callbacks and includes stable IDs, hierarchy, bounds, resolved state, actions, range and collection data. toJson() is suitable for diagnostic reports and tooling.

Semantic setters always mark the cached tree dirty. Pull-based native ports rebuild it on the Codename One EDT only while assistive technology is active or when a tool explicitly requests a snapshot; the browser keeps its off-screen ARIA projection synchronized continuously. Applications should still publish complete semantics unconditionally—this runtime optimization is transparent to application code.

AccessibilityTreeSnapshot tree =
        AccessibilityInspector.snapshot(myForm);

AccessibilityAssertions.assertNoErrors(tree); AccessibilityAssertions.assertNoUnlabeledInteractiveNodes(tree);

String diagnosticJson = tree.toJson(); AccessibilityNodeSnapshot hit = tree.getNodeAt(screenX, screenY);

AccessibilityAssertions.audit() reports machine-readable AccessibilityIssue values for unlabeled controls, invalid ranges, duplicate identifiers or actions, invalid collection positions, missing parents, cycles, unnamed dialogs, and unlabeled custom actions. Warnings also identify suspicious role/state combinations and empty bounds.

The Java SE Component Inspector provides Copy Accessibility Tree JSON and Audit Accessibility Tree actions. Tests should assert resolved semantic properties instead of platform-specific spoken sentences. The scripts/hellocodenameone suite contains the cross-port conformance fixture used by native CI.

11.11. Diagnosing and correcting an inaccessible screen

Accessibility review works best as a repeatable loop instead of a final checklist:

  1. Use the keyboard to reach every operation. Verify visible focus, logical order, escape from modal UI, and operation without pointer gestures.

  2. Open Tools → Component Inspector. Select the form or a suspicious component, then choose Audit Accessibility Tree. Start with errors, such as an unlabeled interactive node, invalid range, or unnamed dialog.

  3. Choose Copy Accessibility Tree JSON and inspect the resolved role, label, value, state, actions, bounds, and child order. This catches cases where the visual component hierarchy doesn’t match the virtual accessibility hierarchy.

  4. Use Simulate → Accessibility Preferences to test high contrast, reduced motion, reduced transparency, inversion, grayscale, and each color-vision filter. Also test Simulate → Larger Text at Accessibility 5. Look for clipped text, color-only state, invisible focus, unreadable overlays, and motion that remains essential to understanding the result.

  5. Correct the component API or theme. Run the audit again, add a focused assertion, and verify the native screen reader before considering the issue closed.

The simulator menu changes the values returned by Display and CN. Color-vision, inversion, and grayscale choices also filter the simulator canvas, so screenshots show approximately what remains distinguishable. These filters are diagnostic approximations; they don’t replace testing with platform display filters or users.

Java SE simulator with the Accessibility Preferences menu open
Figure 171. Accessibility preferences in the Java SE simulator

The following icon-only custom control is painted and responds to a pointer, but initially has no usable name, role, or keyboard-equivalent action. The audit reports it as an unlabeled interactive node. Repair the semantic contract at the custom-control boundary:

iconOnlyButton.getSemantics()
        .setRole(AccessibilityRole.BUTTON)
        .setLabel("Delete message")
        .setHint("Removes this message from the conversation")
        .setIdentifier("message-delete")
        .addAction(new AccessibilityAction(
                AccessibilityAction.ACTIVATE,
                null,
                new AccessibilityAction.Handler() {
                    public boolean perform(Component component, Object argument) {
                        deleteMessage();
                        return true;
                    }
                }));

Don’t fix the warning by adding a label alone if the role, current state, range, or available actions are still wrong. For example, a custom switch needs SWITCH, checked state, and activation; a chart needs virtual descendants for its meaningful data points; a renderer-backed grid needs collection and item metadata.

Component Inspector showing an accessibility audit
Figure 172. Auditing the semantic tree in the Component Inspector

11.11.1. Assertions that prevent regressions

Snapshot the finished form and assert behavior that matters to a user. Prefer stable identifiers and resolved semantics over implementation class names or platform-specific speech:

AccessibilityTreeSnapshot tree = AccessibilityInspector.snapshot(form);
AccessibilityAssertions.assertNoErrors(tree);
AccessibilityAssertions.assertNoUnlabeledInteractiveNodes(tree);

AccessibilityNodeSnapshot save = tree.getNodeByIdentifier("profile-save");
assert save.getRole() == AccessibilityRole.BUTTON;
assert save.getAction(AccessibilityAction.ACTIVATE) != null;

Keep targeted assertions for traversal order, validation errors, live-region behavior, ranges, and stable virtual keys. The scripts/hellocodenameone accessibility fixture invokes every portable preference API and exercises the resolved semantic tree on each native CI platform.

11.11.2. Native verification

The simulator finds portable problems, but the final pass must use VoiceOver, TalkBack, Narrator, Orca, or the browser accessibility tree. Navigate in both directions, activate every action, change adjustable values, enter and leave collections, trigger errors, and confirm that focus moves sensibly after a dialog, navigation, deletion, or live update. Record the platform, assistive-technology version, input method, and failed semantic node in the bug report; attach the inspector JSON when the native result differs from the portable snapshot.

11.12. Platform mappings

TargetNative representationNotable behavior

iOS, iPadOS, and macOS

UIAccessibilityElement instances owned by the rendering view

Traits, values, frames, custom actions, adjustable actions, live announcements, and screen/layout transitions

Android

AccessibilityNodeProvider virtual descendants

Class/role mapping, checked and mixed state, headings, pane titles, errors, ranges, collections, focus, hit testing, and custom actions

Windows native

UI Automation fragment-root provider

Virtual fragments, control types, names, values, state, bounds, navigation, focus lookup, hit testing, and action dispatch

Linux native

Transparent GTK semantic widgets exposed through ATK/AT-SPI

Roles, names, descriptions, state, hierarchy, bounds, and action dispatch

Java SE simulator and desktop

Swing AccessibleContext virtual children

Roles, states, values, geometry, accessible actions, and Component Inspector integration

JavaScript

A synchronized off-screen DOM tree

ARIA roles, names, state, ranges, live regions, collection metadata, traversal order, and action routing; the painted canvas is hidden from the accessibility tree

The API intentionally goes beyond a label-only model: it includes stable virtual children, explicit traversal constraints, collection spans and set metadata, pane transitions, machine-readable inspection, and built-in semantic audits. Platform accessibility tools remain the final verification step because screen readers can apply platform-specific presentation rules.

For underlying platform guidance, see the Apple UIAccessibility protocol, Android accessibility principles, Microsoft UI Automation provider overview, and W3C WAI-ARIA.

12. Animations

There are many ways to animate and liven the data within a Codename One application, layout animations are probably chief among them. But first you need to understand some basics such as layout reflows.

12.1. Layout reflow

Layout in tools such as HTML is implicit, when you add something into the UI it’s automatically placed. Other tools such as Codename One use explicit layout, that means you’ve to explicitly request the UI to layout itself after making changes!

Like many such rules exceptions occur. For example, if the device is rotated or window size changes a layout will occur automatically.

When adding a component to a UI that’s already visible, the component won’t show by default.

When adding a component to a form which isn’t shown on the screen, there is no need to tell the UI to repaint or reflow. This happens implicitly.

The chief advantage of explicit layout is performance.

For example, imagine adding 100 components to a form. If the form was laid out automatically, layout would have happened 100 times instead of once when adding was finished. In fact layout reflows are often considered the #1 performance issue for HTML/JavaScript applications.

Smart layout reflow logic can alleviate some pains of the automatic layout reflows but since the process is implicit it’s almost impossible to optimize complex usages across browsers/devices. A major JavaScript performance tip is to use absolute positioning which is akin to not using layouts at all!

That’s why, when you add components to a form that’s already showing, you should invoke revalidate() or animate the layout appropriately. This also enables the layout animation behavior explained below.

12.2. Layout animations

To understand animations you need to understand a couple of things about Codename One components. When you add a component to a container, it’s added but not positioned anywhere. A novice might notice the setX/setY/setWidth/setHeight methods on a component and try to position it.

This won’t work since these methods are meant for the layout manager, which is implicitly invoked when a form is shown (internally in Codename One). The layout manager uses these methods to position/size the components based on the hints given to it.

If you add components to a form that’s showing, it’s your responsibility to invoke revalidate/layoutContainer to arrange the newly added components (see Layout Reflows).

animateLayout() method is a fancy form of revalidate that animates the components into their laid out position. After changing the layout & invoking this method the components move to their new sizes/positions seamlessly. Form exposes convenience wrappers such as animateLayout*() that forward to the underlying content pane, so you can call the methods directly on the form unless you specifically need to animate a nested container.

This sort of behavior creates a special case where setting the size/position makes sense. When you set the size/position in the demo code here you’re positioning the components at the animation start position above the frame:

Form hi = new Form("Layout Animations", new BoxLayout(BoxLayout.Y_AXIS));
Button fall = new Button("Fall"); (1)
fall.addActionListener((e) -> {
    for (int iter = 0; iter < 10; iter++) {
        Label b = new Label("Label " + iter);
        b.setWidth(fall.getWidth());
        b.setHeight(fall.getHeight());
        b.setY(-fall.getHeight());
        hi.add(b);
    }
    hi.animateLayout(20000); (2)
});
hi.add(fall);

Notice these things about this example:

1You used a button to do the animation rather than doing it on show. Since show() implicitly lays out the components it wouldn’t have worked.
2You used hi.animateLayout(20000);, which delegates to the Form content pane. If you need to animate a specific container (for example, a nested layout), call animateLayout() on that container instead.

This results in:

Frame 1 Frame 2 Frame 3 Frame 4 Frame 5 Frame 6 Frame 7

12.2.1. Unlayout animations

While layout animations are powerful effects for adding elements into the UI and drawing attention to them. The inverse of removing an element from the UI is often more important. For example, when you delete or remove an element you want to animate it out.

Layout animations don’t do that since they will try to bring the animated item into place. The exact opposite of a layout animation is the "unlayout animation." Container.animateUnlayout(int, int, Runnable) and the Form.animateUnlayout*() helpers trigger this transition either asynchronously (with a completion callback) or synchronously via the AndWait variants.

The "unlayout animation" takes a valid laid out state and shifts the components to an invalid state that you defined in advance. For example, you can fix the example above to flip the "fall" button into a "rise" button when the buttons come into place and this will allow the buttons to float back up to where they came from in the exact reverse order.

An unlayout animation always leaves the container in an invalidated state.
Form hi = new Form("Layout Animations", new BoxLayout(BoxLayout.Y_AXIS));
Button fall = new Button("Fall");
fall.addActionListener((e) -> {
    if (hi.getContentPane().getComponentCount() == 1) {
        fall.setText("Rise");
        for (int iter = 0; iter < 10; iter++) {
            Label b = new Label("Label " + iter);
            b.setWidth(fall.getWidth());
            b.setHeight(fall.getHeight());
            b.setY(-fall.getHeight());
            hi.add(b);
        }
        hi.animateLayout(20000);
    } else {
        fall.setText("Fall");
        for (int iter = 1; iter < hi.getContentPane().getComponentCount(); iter++) { (1)
            Component c = hi.getContentPane().getComponentAt(iter);
            c.setY(-fall.getHeight()); (2)
        }
        hi.animateUnlayoutAndWait(20000, 255); (3)
        hi.removeAll(); (4)
        hi.add(fall);
        hi.revalidate();
    }
});
hi.add(fall);

You will notice some similarities with the unlayout animation but the differences represent the exact opposite of the layout animation:

1You loop over existing components (not newly created ones)
2You set the desired end position not the desired starting position
3You used the animateUnlayoutAndWait(…​) variant to block until completion; animateUnlayout(duration, opacity, callback) provides the non-blocking alternative when you want to continue and run code from a callback instead.
4After the animation completes you need to actually remove the elements since the UI is now in an invalid position with elements outside of the screen but still physically there!

12.2.2. Hiding & visibility

A common trick for animating Components in Codename One is to set their preferred size to 0 and then invoke animateLayout() thus triggering an animation to hide said Component. several issues with this trick but one of the biggest ones is the fact that setPreferredSize has been deprecated for quite a while.

Instead of using that trick you can use setHidden/isHidden who effectively encapsulate this functionality and a bit more.

One of the issues setHidden tries to solve is the fact that preferred size doesn’t include the margin in the total and thus a component might still occupy space despite being hidden. When you request the margin change the current margins are cached, the component is given zero margins while hidden, and those cached values are restored when it’s shown again—without resetting the UIID or other style state.

This functionality might be undesirable which is why there is a version of the setHidden method that accepts a boolean flag indicating whether the margin cache should be manipulated. You can effectively hide/show a component without deprecated code using something like this:

Button toHide = new Button("Will Be Hidden");
Button hide = new Button("Hide It");
hide.addActionListener((e) -> {
    hide.setEnabled(false);
    boolean t = !toHide.isHidden();
    toHide.setHidden(t);
    toHide.getParent().animateLayoutAndWait(200);
    toHide.setVisible(!t);
    hide.setEnabled(true);
});
Notice that the code above uses setVisible(), which shouldn’t be confused with setHidden. setVisible() toggles the visibility of the component it would still occupy the same amount of space

12.2.3. Synchronicity in animations

Most animations have two or three variants:

  • Standard animation for example, animateLayout(int) or the non-blocking animateUnlayout(int, int, Runnable)

  • And wait variant for example, animateLayoutAndWait(int) / animateUnlayoutAndWait(int, int)

  • Callback variant for example, animateLayoutFade(int, int, Runnable)

The standard animation is invoked when you don’t care about the completion of the animation. You can do this for a standard animation.

Unlayout animations always leave the container in an invalid state, so even the standard helper expects you to tidy up either in the callback or after the animation finishes.

The AndWait variant blocks the calling thread until the animation completes. This is useful for sequencing animations one after the other e.g this code from the kitchen sink demo:

arrangeForInterlace(effects);
effects.animateUnlayoutAndWait(800, 20);
effects.animateLayoutFade(800, 20);

First the UI goes through an "unlayout" animation, once that completes the layout itself is performed.

The AndWait calls needs to be invoked on the Event Dispatch Thread despite being "blocking." This is a common convention in Codename One powered by a unique capability of Codename One: invokeAndBlock.
You can learn more about invokeAndBlock in the EDT section.

The callback variant is like the invokeAndBlock variant but uses a more conventional callback semantic which is more familiar to some developers. It accepts a Runnable callback that will be invoked after the fact. For example, you can change the unlayout call from before to use the callback semantics as such:

hi.animateUnlayout(20000, 255, () -> {
    hi.removeAll();
    hi.add(fall);
    hi.revalidate();
});
Animation fade and hierarchy

There are several more variations on the standard animate methods. Several methods accept a numeric fade argument. This is useful to fade out an element in an "unlayout" operation or fade in a regular animation.

The value for the fade argument is a number between 0 and 255 where 0 represents full transparency and 255 represents full opacity.

Some animate layout methods are hierarchy based. They work like the regular animateLayout methods but recurse into the entire Container hierarchy. These methods work well when you have components in a nested hierarchy that need to animate into place. This is demonstrated in the opening sequence of the kitchen sink demo:

for(int iter = 0 ; iter < demoComponents.size() ; iter++) {
    Component cmp = (Component)demoComponents.elementAt(iter);
    if(iter < componentsPerRow) {
        cmp.setX(-cmp.getWidth());
    } else {
        if(iter < componentsPerRow * 2) {
            cmp.setX(dw);
        } else {
            cmp.setX(-cmp.getWidth());
        }
    }
}
boxContainer.setShouldCalcPreferredSize(true);
boxContainer.animateHierarchyFade(3000, 30);

The demoComponents Vector contains components from separate containers and this code would not work with a simple animate layout.

Avoid the hierarchy version where possible. Its slower but more importantly, it’s flaky. Since the size/position of the Container might be affected by the layout the animation could get clipped and skip. These are hard issues to debug.

12.2.4. Sequencing animations via AnimationManager

All the animations go through a per-form queue: the AnimationManager. This effectively prevents two animations from mutating the UI in parallel so you won’t have collisions between two conflicting sides. Things get more interesting when you try to do something like this:

cnt.add(myButton);
int componentCount = cnt.getComponentCount();
cnt.animateLayout(300);
cnt.removeComponent(myButton);
if(componentCount == cnt.getComponentCount()) {
    // this will happen...
}

The reason this happens is that the second remove gets postponed to the end of the animation so it won’t break the animation. This works for remove and add operations on a Container as well as other animations.

The simple yet problematic fix would be:

cnt.add(myButton);
int componentCount = cnt.getComponentCount();
cnt.animateLayoutAndWait(300);
cnt.removeComponent(myButton);
if(componentCount == cnt.getComponentCount()) {
    // this probably won't happen...
}

Why might that still fail?

Events come in constantly during the run of the EDT [6], so an event might come in that might trigger an animation in your code. Even if you’re on the EDT keep in mind that you don’t actually block it and an event might come in.

In those cases an animation might start and you might be unaware of that animation and it might still be in action when you expect remove to work.

Animation manager to the rescue

AnimationManager has built-in support to fix this exact issue.

You can flush the animation queue and run synchronously after all the animations finished and before new ones come in by using something like this:

cnt.add(myButton);
int componentCount = cnt.getComponentCount();
cnt.animateLayout(300);
cnt.getAnimationManager().flushAnimation(() -> {
    cnt.removeComponent(myButton);
    if(componentCount == cnt.getComponentCount()) {
        // this shouldn't happen...
    }
});

12.3. Low-level animations

The Codename One event dispatch thread has a special animation "pulse" allowing an animation to update its state and draw itself. Code can make use of this pulse to implement repetitive polling tasks that have little to do with drawing.

This is helpful since the callback will always occur on the event dispatch thread.

Every component in Codename One contains an animate() method that returns a boolean value, you can also implement the Animation interface in an arbitrary component to implement your own animation. To receive animation events you need to register yourself within the parent form, it’s the responsibility of the parent for to call animate().

If the animate method returns true then the animation will be painted (the paint method of the Animation interface would be invoked).

It’s important to deregister animations when they aren’t needed to conserve battery life.

If you derive from a component, which has its own animation logic you might damage its animation behavior by deregistering it, so use care with the low-level API’s.

For example, you can add more animation logic using code like this:

myForm.registerAnimated(this);
private int spinValue;
@Override
public boolean animate() {
   if(userStatusPending) {
       spinValue++;
       super.animate();
       return true;
   }
   return super.animate();
}

12.3.1. Why not just write code in paint

Animations comprise two parts, the logic (deciding the position, etc.) and the painting. The paint method should be dedicated to painting, not to the actual moving of the components.

The separation of concerns allows you to avoid redundant painting for example, if animate didn’t trigger a change return false to avoid the overhead related to animations.

You discuss low-level animations in more details within the animation section of the clock demo.

12.4. Transitions

Transitions allow you to replace one component with another, most typically forms or dialogs are replaced with a transition but a transition can be applied to replace any arbitrary component.

Developers can implement their own custom transition and install it to components by deriving the Transition class, although most commonly the built in CommonTransitions class is used for almost everything.

You can define transitions for forms/dialogs/menus globally either via the theme constants or via the LookAndFeel class. Or you can install a transition on top-level components via setter methods.

In/Out Transitions

When defining a transition you define the entering transition and the exiting transition. For most cases only one of those is necessary and you default to the exiting (out transition) as a convention.

For most cases the method setFormTransitonIn should go unused. That API exists for some elaborate custom transitions that might need to have a special effect both when transitioning in and out of a specific form. But, most of these effects are easier to achieve with layout animations (for example, components dropping into place etc.).

For Dialog the transition in shows its appearance and the transition out shows its disposal. In that case both transitions make a lot of sense.

Back/Forward Transitions

Transitions have a direction and can all be played either in incoming or outgoing direction. A transition can be flipped (played in reverse) when use an RTL language [7] or when you traverse backwards in the form navigation hierarchy.

Normally Form.show() displays the next Form with an incoming transition based on the current RTL mode. If you use Form.showBack() it will play the transition in reverse.

When working with high-level animations you can select Slow Motion option in the simulator to slow down animations and inspect their details

Themes define the default transitions used when showing a form, these differ based on the OS. In most platforms the default is Slide whereas in iOS the default is SlideFade which slides the content pane and title while fading in/out the content of the title area.

SlideFade is problematic without a title area. If you have a Form that lacks a title area you would recommend to disable SlideFade at least for that Form.

Check out the full set of theme constants in the Theme Constants Section.

12.4.1. Replace

To apply a transition to a component you can use the Container.replace() method as such:

Form hi = new Form("Replace", new BoxLayout(BoxLayout.Y_AXIS));
Button replace = new Button("Replace Pending");
Label replaceDestiny = new Label("Destination Replace");
hi.add(replace);
replace.addActionListener((e) -> {
    replace.getParent().replaceAndWait(replace, replaceDestiny, CommonTransitions.createCover(CommonTransitions.SLIDE_VERTICAL, true, 800));
    replaceDestiny.getParent().replaceAndWait(replaceDestiny, replace, CommonTransitions.createUncover(CommonTransitions.SLIDE_VERTICAL, true, 800));
});
Replace even works when you have a layout constraint in place for example, replacing a component in a border layout will do the "right thing." But, some layouts such as TableLayout might be tricky in such cases so recommend wrapping a replaceable Component in a border layout and replacing the content.

Container.replace() can also be used with a null transition at which point it replaces instantly with no transition.

12.4.2. Slide transitions

The slide transitions are used to move the Form/Component in a sliding motion to the side or up/down. Four basic types of slide transitions exist:

  1. Slide - the most commonly used transition

  2. Fast Slide - historically this provided better performance for old device types. It’s no longer recommended for newer devices

  3. Slide Fade - the iOS default where the title area features a fade transition

  4. Cover/Uncover - a kind of slide transition where the source or destination form slides while the other remains static in place

The code below demonstrates the usage of all the main transitions:

    Toolbar.setGlobalToolbar(true);
    Form hi = new Form("Transitions", new BoxLayout(BoxLayout.Y_AXIS));
    Style bg = hi.getContentPane().getUnselectedStyle();
    bg.setBgTransparency(255);
    bg.setBgColor(0xff0000);
    Button showTransition = new Button("Show");
    Picker pick = new Picker();
    pick.setStrings("Slide", "SlideFade", "Cover", "Uncover", "Fade", "Flip");
    pick.setSelectedString("Slide");
    TextField duration = new TextField("10000", "Duration", 6, TextArea.NUMERIC);
    CheckBox horizontal = CheckBox.createToggle("Horizontal");
    pick.addActionListener((e) -> {
        String s = pick.getSelectedString().toLowerCase();
        horizontal.setEnabled(s.equals("slide") || s.indexOf("cover") > -1);
    });
    horizontal.setSelected(true);
    hi.add(showTransition).
        add(pick).
        add(duration).
        add(horizontal);

    Form dest = new Form("Destination");
    bg = dest.getContentPane().getUnselectedStyle();
    bg.setBgTransparency(255);
    bg.setBgColor(0xff);
    dest.setBackCommand(
            dest.getToolbar().addCommandToLeftBar("Back", null, (e) -> hi.showBack()));

    showTransition.addActionListener((e) -> {
        int h = CommonTransitions.SLIDE_HORIZONTAL;
        if(!horizontal.isSelected()) {
            h = CommonTransitions.SLIDE_VERTICAL;
        }
        switch(pick.getSelectedString()) {
            case "Slide":
                hi.setTransitionOutAnimator(CommonTransitions.createSlide(h, true, duration.getAsInt(3000)));
                dest.setTransitionOutAnimator(CommonTransitions.createSlide(h, true, duration.getAsInt(3000)));
                break;
            case "SlideFade":
                hi.setTransitionOutAnimator(CommonTransitions.createSlideFadeTitle(true, duration.getAsInt(3000)));
                dest.setTransitionOutAnimator(CommonTransitions.createSlideFadeTitle(true, duration.getAsInt(3000)));
                break;
            case "Cover":
                hi.setTransitionOutAnimator(CommonTransitions.createCover(h, true, duration.getAsInt(3000)));
                dest.setTransitionOutAnimator(CommonTransitions.createCover(h, true, duration.getAsInt(3000)));
                break;
            case "Uncover":
                hi.setTransitionOutAnimator(CommonTransitions.createUncover(h, true, duration.getAsInt(3000)));
                dest.setTransitionOutAnimator(CommonTransitions.createUncover(h, true, duration.getAsInt(3000)));
                break;
            case "Fade":
                hi.setTransitionOutAnimator(CommonTransitions.createFade(duration.getAsInt(3000)));
                dest.setTransitionOutAnimator(CommonTransitions.createFade(duration.getAsInt(3000)));
                break;
            case "Flip":
                hi.setTransitionOutAnimator(new FlipTransition(-1, duration.getAsInt(3000)));
                dest.setTransitionOutAnimator(new FlipTransition(-1, duration.getAsInt(3000)));
                break;
        }
        dest.show();
    });
    hi.show();
The slide transition moves both incoming and outgoing forms together
Figure 173. The slide transition moves both incoming and outgoing forms together
The slide transition can be applied vertically as well
Figure 174. The slide transition can be applied vertically as well
Slide fade fades in the destination title while sliding the content pane its the default on iOS
Figure 175. Slide fade fades in the destination title while sliding the content pane it’s the default on iOS
SlideFade is problematic without a title area. If you have a Form that lacks a title area you would recommend to disable SlideFade at least for that Form.
With cover transitions the source form stays in place as it’s covered by the destination. This transition can be played both horizontally and vertically
Figure 176. With cover transitions the source form stays in place as it’s covered by the destination. This transition can be played both horizontally and vertically
Uncover is the inverse of cover. The destination form stays in place while the departing form moves away
Figure 177. Uncover is the inverse of cover. The destination form stays in place while the departing form moves away

12.4.3. Fade and flip transitions

The fade transition is pretty trivial and accepts a time value since it has no directional context.

Fade transition is probably the simplest one around
Figure 178. Fade transition is probably the simplest one around

The FlipTransition is also pretty simple but unlike the others it isn’t a part of the CommonTransitions. It has its own FlipTransition class.

This transition looks different on devices as it uses native perspective transforms available there
Fade transition is probably the simplest one around
Figure 179. Fade transition is probably the simplest one around

12.4.4. Bubble transition

BubbleTransition morphs a component into another component using a circular growth motion.

The BubbleTransition accepts the component that will grow into the bubble effect as one of its arguments. It’s primarily designed for Dialog transitions although it could work for more creative use cases:

The code below manipulates styles and look. This is done to make the code more "self contained." Real world code should probably use the theme
    Form hi = new Form("Bubble");
    Button showBubble = new Button("+");
    showBubble.setName("BubbleButton");
    Style buttonStyle = showBubble.getAllStyles();
    buttonStyle.setBorder(Border.createEmpty());
    buttonStyle.setFgColor(0xffffff);
    buttonStyle.setBgPainter((g, rect) -> {
        g.setColor(0xff);
        int actualWidth = rect.getWidth();
        int actualHeight = rect.getHeight();
        int xPos, yPos;
        int size;
        if(actualWidth > actualHeight) {
            yPos = rect.getY();
            xPos = rect.getX() + (actualWidth - actualHeight) / 2;
            size = actualHeight;
        } else {
            yPos = rect.getY() + (actualHeight - actualWidth) / 2;
            xPos = rect.getX();
            size = actualWidth;
        }
        g.setAntiAliased(true);
        g.fillArc(xPos, yPos, size, size, 0, 360);
    });
    hi.add(showBubble);
    hi.setTintColor(0);
    showBubble.addActionListener((e) -> {
        Dialog dlg = new Dialog("Bubbled");
        dlg.setLayout(new BorderLayout());
        SpanLabel sl = new SpanLabel("This dialog should appear with a bubble transition from the button", "DialogBody");
        sl.getTextUnselectedStyle().setFgColor(0xffffff);
        dlg.add(BorderLayout.CENTER, sl);
        dlg.setTransitionInAnimator(new BubbleTransition(500, "BubbleButton"));
        dlg.setTransitionOutAnimator(new BubbleTransition(500, "BubbleButton"));
        dlg.setDisposeWhenPointerOutOfBounds(true);
        dlg.getTitleStyle().setFgColor(0xffffff);

        Style dlgStyle = dlg.getDialogStyle();
        dlgStyle.setBorder(Border.createEmpty());
        dlgStyle.setBgColor(0xff);
        dlgStyle.setBgTransparency(0xff);
        dlg.showPacked(BorderLayout.NORTH, true);
    });

    hi.show();
Bubble transition converting a circular button to a Dialog
Figure 180. Bubble transition converting a circular button to a Dialog

12.4.5. Morph transitions

Android’s material design has a morphing effect where an element from the previous form (activity) animates into a different component on a new activity. Codename One has a morph effect in the Container class but it doesn’t work as a transition between forms and doesn’t allow for multiple separate components to transition at once.

Morph Transition
Figure 181. Morph Transition

To support this behavior you have the MorphTransition class that provides this same effect coupled with a fade to the rest of the UI (see Figure 181, “Morph Transition”).

Since the transition is created before the form exists you can’t reference explicit components within the form when creating the morph transition (to show which component becomes which) so you need to refer to them by name. This means you need to use setName(String) on the components in the source/destination forms so the transition will be able to find them:

Form demoForm = new Form(currentDemo.getDisplayName());
demoForm.setScrollable(false);
demoForm.setLayout(new BorderLayout());
Label demoLabel = new Label(currentDemo.getDisplayName());
demoLabel.setIcon(currentDemo.getDemoIcon());
demoLabel.setName("DemoLabel");
demoForm.addComponent(BorderLayout.NORTH, demoLabel);
demoForm.addComponent(BorderLayout.CENTER, wrapInShelves(n));
// ...
demoForm.setBackCommand(backCommand);
demoForm.setTransitionOutAnimator(
    MorphTransition.create(3000).morph(
        currentDemo.getDisplayName(),
        "DemoLabel"));
f.setTransitionOutAnimator(
    MorphTransition.create(3000).
        morph(currentDemo.getDisplayName(),
        "DemoLabel"));
demoForm.show();
Snapshot mode

By default MorphTransition paints both endpoints by re-rendering the live source / destination components every frame at the interpolated bounds. That works well when the source is fully visible — a card in the body of one form morphing to a card in the next.

It runs into edge cases when the source lives inside a scrolling container that has children extending past the source’s bounds, or when the source has dynamic content (a video frame, a BrowserComponent, a custom-painted background) that should be visually frozen for the duration of the animation. The legacy live-paint path can leak off-viewport pixels into the morph because the layered pane that holds the source during the animation doesn’t carry the original parent’s clip.

To opt into the image-snapshot path call snapshotMode(true) on the builder:

MorphTransition morph = MorphTransition.create(300)
        .snapshotMode(true)
        .morph("card");
nextForm.setTransitionInAnimator(morph);
nextForm.show();

snapshotMode(true) captures each (source, dest) pair as a clipped Image at initTransition(), then the tween draws those images at the interpolated bounds rather than re-painting the live components. Off-viewport children of the source are clipped at capture time (the image’s own bounds are the clip), so they can’t leak into the morph.

The default MorphTransition behavior (live paint, no snapshots) is unchanged for back-compat. Use snapshot mode opportunistically when the live-paint output exhibits the off-viewport leak, or when the source’s children produce frame-by-frame visual change you want to freeze.

12.4.6. SwipeBackSupport

iOS7+ allows swiping back one form to the previous form, Codename One has an API to enable back swipe transition:

SwipeBackSupport.bindBack(currentForm, destination);

That one command will enable swiping back from currentForm. LazyValue allows you to pass a value lazily:

/**
 * Useful when passing a value that might not exist to a function, e.g. When we
 * pass a form that we might need to construct dynamically later on.
 */
public interface LazyValue<T> {
    /**
     * Returns the actual value.
     *
     * @param args optional arguments for the creation of the lazy value
     * @return the value
     */
    T get(Object... args);
}

This effectively allows you to pass a form and create it as necessary (for example, for a GUI builder app you don’t have the actual previous form instance), notice that the arguments aren’t used for this case but will be used in other cases.

The code below should work for the transition sample above. Notice that this API was designed to work with "Slide Fade" transition and might have issues with other transition types:

SwipeBackSupport.bindBack(dest, (args) -> hi);

Theming and styling

13. Advanced theming

Before getting into CSS, this section introduces a few advanced theme concepts. Those ideas also apply to CSS because CSS uses features such as theme constants.

13.1. Working with UIIDs

A UIID (User Interface Identifier) uniquely identifies a group of theme definitions for UI components. For example, a Button UIID can be linked to a component, and the theme can then define that component’s appearance.

A major advantage of UIIDs is the ability to change a component’s UIID. For example, to create a multiline label, use code like this:

TextArea t = new TextArea(); // ...
t.setUIID("Label");
t.setEditable(false);
Components such as SpanLabel use this approach internally.

UIIDs can be customized in the GUI builder and support per-component styling.

The component class name often matches the UIID, but the two remain separate entities.

Looking a UIID up isn’t a single lookup. The state, the appearance and any derive chain each get a turn before the default style is reached:

The order UIManager searches to resolve a UIID into a Style
Figure 182. The order UIManager searches when a component asks for its style

13.2. Theme layering

Theme layering helps in two common cases:

  • A platform needs a slightly different theme.

  • A specific use case needs theme customization. For example, a user might select larger fonts.

Theme layering is straightforward and doesn’t require rebuilding the entire theme. A theme can be applied on top of another theme, like CSS cascading. To create a layer, add a new theme with the Add Theme button.

Remove the includeNativeBool constant from the new theme.

In the new theme, define the changes you need. For example, to use a larger default font, define that property for the relevant UIIDs and leave the other properties unchanged.

In a non-GUI-builder app, the theme loading code looks like this by default:

Resources theme = UIManager.initFirstTheme("/theme");

Update it to look like this:

Resources theme = UIManager.initNamedTheme("/theme", "Theme");
This example assumes that the main theme is named Theme, not the layer theme that was added.

The original code relies on the theme being at position 0 in the theme name array. That might not always be true.

To add the theme layer, use:

UIManager.getInstance().addThemeProps(theme.getTheme("NameOfLayerTheme"));

The addThemeProps call places the secondary theme on top of the primary "Theme" and keeps the original UIIDs in "Theme" intact.

To apply theme changes to a running application, call Form’s `refreshTheme(). This updates the UI and provides visual feedback for the theme changes.

13.3. Override resources in platform

When you adapt an application to different OS conventions, one common need is to use different icons. Sometimes you also need to change behavior based on device type, for example to use a different UI structure for a tablet.

Codename One lets you override a resource for a specific platform. You can redefine a resource for that platform and add platform-specific resources.

Override resources for specific platform
Figure 183. Override resources for specific platform

Overridden resources take precedence over embedded resources, which lets you change the look or even behavior for a specific platform or OS.

Overriding the theme is dangerous because a theme has external dependencies, such as image borders. The solution is to use theme layering and override the layer.

To override a resource, select the platform where the override applies.

Override for platform, allows you to override the checked resources and replace them with another resource
Figure 184. Override for platform, allows you to override the checked resources and replace them with another resource

You can then click the green checkbox to mark that resource as specific to this platform. All resources added while that platform is selected apply to that platform. If you change your mind, delete the override in override mode and it will stop applying.

13.4. Theme constants

The Codename One Designer has a tab for creating constants. These constants can add global values and behavior hints to Codename One and its components. Constants are always strings. Some conventions let the UI adapt to input types. For example, a constant that ends with Bool is treated as a boolean value and displays as a checkbox. An Int suffix displays a numeric picker, and an Image suffix shows a combo box for picking an image.

The combo box in the designer for adding a theme constant is editable, so you can type any value you want.

To use a constant, call the appropriate UIManager methods:

  • getThemeConstant

  • isThemeConstant

  • getThemeImageConstant

Internally, Codename One has many built-in constants, and the list keeps growing. As features are added, the list is updated, but theme constants are inherently ad hoc and some may not appear here.

Table 6. Theme Constants
ConstantDescription/Argument

alwaysTensileBool

Enables tensile drag even when there is no scrolling in the container (only for scrollable containers)

backGestureThresholdInt

The threshold for the back gesture in the SwipeBackSupport class, defaults to 5

backUsesTitleBool

Indicates to the GUI builder that the back command should use the title of the previous form and not just the word "Back"

buttonRippleBool

true to activate the material design ripple effect on the buttons. This effect draws a growing circle from the point of touch onwards. This is false by default except for Android where it defaults to true. This is equivalent to setButtonRippleEffectDefault(bool) in the Button class

capsButtonTextBool

true to activate the caps text mode in the buttons. When activated setText on Button and all the constructors will invoke upcase() on all the strings effectively making the application buttons use uppercase exclusively. This is false by default except for Android where it defaults to true. It’s equivalent to Button.setCapsTextDefault(boolean) and can be tuned to an individual Component via Component.setRippleEffect(boolean)

capsButtonUiids

A list of the UIID’s that should be capitalized by default (in supported platforms) other than the Button and RaisedButton which are already capitalized. This list can be separated by spaces or commas for example, capsButtonUiids=UpcaseButton,OtherCustomButton

defaultCommandImage

Image to give a command with no icon

defaultNativeWindowModeBool

true to back every newly created Dialog and InteractionDialog with a real operating system window on desktop platforms. Ignored where there is no windowing system, so it changes nothing on mobile or the web. Equivalent to Dialog.setDefaultNativeWindowMode(boolean), and overridden for one dialog by setNativeWindowMode(boolean)

dialogButtonCommandsBool

Place commands in the dialogs as buttons

dialogBlurRadiusInt

Sets the default Gaussian blur radius for the background of the dialogs. The default value is -1 indicating no blur

dialogPosition

Place the dialog in an arbitrary border layout position (for example, North, South, Center, etc.)

centeredPopupBool

Popup of the combo box will appear in the center of the screen

changeTabOnFocusBool

Useful for feature phones, allows changing the tab when the focus changes, without pressing a key

checkBoxCheckDisImage

CheckBox image to use instead of Codename One drawing it on its own

checkBoxCheckedImage

CheckBox image to use instead of Codename One drawing it on its own

checkBoxOppositeSideBool

Indicates the check box should be drawn on the opposite side to the text and not next to the text

checkBoxUncheckDisImage

CheckBox image to use instead of Codename One drawing it on its own

checkBoxUncheckedImage

CheckBox image to use instead of Codename One drawing it on its own

comboImage

Combo image to use instead of Codename One drawing it on its own

commandBehavior

Deprecated: don’t use this constant as it conflicts with the Toolbar. Indicates how commands should act, as a touch menu, native menu etc. Possible values: SoftKey, Touch, Bar, Title, Right, Native

ComponentGroupBool

Enables component group, which allows components to be logically grouped together, so the UIID’s of components would be modified based on their group placement. This allows for some unique styling effects where the first/last elements have different styles from the rest of the elements. It’s disabled by default, thus leaving its usage up to the designer

dialogTransitionIn

Default transition for dialog

dialogTransitionInImage

Default transition Image for dialog, causes a Timeline transition effect

dialogTransitionOut

Default transition for dialog

defaultCommandImage

An image to place on a command if none is defined, only applies to touch commands

defaultEmblemImage

The emblem painted on the side of the multibutton, by default this is an arrow on some platforms

emblemUiid

Overrides the UIID applied to emblem labels rendered by composite buttons such as MultiButton and SpanMultiButton.

iconUiid

Allows theming the icon component used by SpanLabel, SpanButton, MultiButton, and SpanMultiButton when they fetch icon styling via theme constants.

iosScrollMotionBool

Enables iOS-style scroll physics (nonlinear rubber-band during drag and critically damped tensile snap-back). When unset it defaults to true on the iOS platform and false elsewhere. Set explicitly to override the platform default. See Section 13.4.1, “Scroll motion tuning”

imageviewerNavigationArrowsBool

Enables side navigation arrows by default for ImageViewer instances using the ImageViewer UIID (or a lowercased custom UIID prefix).

imageviewerThumbnailBarHeightMM

Sets the default thumbnail strip height (in millimeters) for ImageViewer navigation thumbnails.

imageviewerThumbnailsBool

Enables the bottom thumbnail strip by default for ImageViewer instances using the ImageViewer UIID (or a lowercased custom UIID prefix).

textUiid

Overrides the text component UIID for SpanLabel, SpanButton, MultiButton, and SpanMultiButton instances when provided as a theme constant.

useLargerTextScaleBool

When true, UIManager applies the larger text accessibility scale from Display.getLargerTextScale() when Display.isLargerTextEnabled() is enabled.

dialogTransitionOutImage

Default transition Image for dialog, causes a Timeline transition effect

DecayMotionScaleFactorInt

Velocity-to-distance multiplier for the exponential-decay scroll motion (see Section 13.4.1, “Scroll motion tuning”). Default is 950. Larger values make flicks travel farther for the same release velocity

disabledColor

Color to use when disabling entries by default

dlgButtonCommandUIID

The UIID used for dialog button commands

dlgCommandButtonSizeInt

Minimum size to give to command buttons in the dialog

dlgCommandGridBool

Places the dialog commands in a grid for uniform sizes

dlgInvisibleButtons

Includes an RRGGBB color for the line separating dialog buttons, as is the case with Android 4 and iOS 7 buttons in dialogs

dlgSlideDirection

Slide hints

dlgSlideInDirBool

Slide hints

dlgSlideOutDirBool

Slide hints

drawMapPointerBool

Indicates whether a pointer should appear in the center of the map component

fadeScrollBarBool

Boolean indicating if the scrollbar should fade when there is inactivity

setFadeScrollBarSpeed(int) (API)

Sets the per-frame opacity decrement for scrollbar fading (not milliseconds). Approximate fade duration is durationMs ~= (255 / speed) * (1000 / frameRate). At 60 FPS: speed 5 ~= 850ms, 3 ~= 1400ms, 2 ~= 2100ms, 1 ~= 4250ms

fadeScrollEdgeBool

Places a fade effect at the edges of the screen to show that it’s possible to scroll until you reach the edge (common on Android)

fadeScrollEdgeInt

Amount of pixels to fade out at the edge

firstCharRTLBool

Indicates to the GenericListCellRenderer that it should determine RTL status based on the first character in the sentence

noTextModeBool

Indicates that the on/off switch in iOS shouldn’t draw text on top of the switch, which is the case for iOS 7+ but not for prior versions

fixedSelectionInt

Number corresponding to the fixed selection constants in List

formTransitionIn

Default transition for form

formTransitionInImage

Default transition Image for form, causes a Timeline transition effect

formTransitionOut

Default transition for form

formTransitionOutImage

Default transition Image for form, causes a Timeline transition effect

globalToobarBool

Indicates that the Toolbar API should be on/off by default for all forms

hasRaisedButtonBool

Is true in platforms where the theme has the RaisedButton UIID defined. This is only true in the native Android theme to allow some material design guidelines

hideBackCommandBool

Hides the back command from the side menu when possible

hideEmptyTitleBool

Indicates that a title with no content should be hidden even if the border for the title occupies space

hideLeftSideMenuBool

Hides the side menu icon that appears on the left side of the UI

hideRightSideMenuBool

Hides the toolbar menu icon that appears on the right side of the title area

ignorListFocusBool

Hide the focus component of the list when the list doesn’t have focus

infiniteImage

The image used by the infinite progress component, the component will rotate it as needed

infiniteMaterialDesignSize

Size (in millimeters) of the generated material design spinner when InfiniteProgress runs in material mode. Defaults to 6.667mm

infiniteMaterialImageSize

Size (in millimeters) of the generated spinner image when no custom infiniteImage is provided. Defaults to 7mm

infiniteDefaultColor

Hex RGB color used for the autogenerated material spinner when infiniteImage isn’t supplied. Defaults to 777777

interactionDialogSpeedInt

Controls the duration in milliseconds that InteractionDialog uses when animating into and out of the layered pane (including the directional dispose helpers). Defaults to 400 if the constant isn’t defined.

includeNativeBool

True to derive from the platform native theme, false to create a blank theme that only uses the basic defaults

labelGap

Positive floating point value representing the default gap value between the label text and the icon in millimeters

listItemGapInt

Built-in item gap in the list, this defaults to 2, which predated padding/margin in Codename One

listLongPressBool

Indicates whether a list should handle long press events, defaults to true

mapTileLoadingImage

An image to preview while loading the MapComponent tile

mapTileLoadingText

The text of the tiles in the MapComponent during loading, defaults to "Loading…​"

mapZoomButtonsBool

Indicates whether buttons should be drawn on the map component

mediaBackImage

Media icon used by the media player class

mediaFwdImage

Media icon used by the media player class

mediaPauseImage

Media icon used by the media player class

mediaPlayImage

Media icon used by the media player class

menuButtonBottomBool

When set to true this flag aligns the menu button to the bottom portion of the title. Defaults to false

menuButtonTopBool

When set to true this flag aligns the menu button to the top portion of the title. Defaults to false

landscapeTitleUiidBool

When true the toolbar swaps to landscape-specific UIIDs (such as ToolbarLandscape/BackCommandLandscape) for layout and icon styling

menuHeightPercent

Allows positioning and sizing the menu

menuImage

The three dot menu image used in Android and the Toolbar to show more command entries

menuImageSize

The size in millimeters (floating point value) of the generated side menu image, this is only used if you don’t supply a custom image. The default value is 4.5.

menuPrefSizeBool

Allows positioning and sizing the menu

menuSlideDirection

Defines menu entrance effect

menuSlideInDirBool

Defines menu entrance effect

menuSlideOutDirBool

Defines menu entrance effect

menuTransitionIn

Defines menu entrance effect

menuTransitionInImage

Defines menu entrance effect

menuTransitionOut

Defines menu exit effect

menuTransitionOutImage

Defines menu entrance effect

menuWidthPercent

Allows positioning and sizing the menu

iosStyleBackArrowBool

Renders the back command using the iOS-style chevron arrow (with a small icon gap) when the toolbar shows back text

minimizeOnBackBool

Indicates whether the form should minimize the entire application when the physical back button is pressed (if available) and no command is defined as the back command. Defaults to true

onOffIOSModeBool

Indicates whether the on/off switch should use the iOS or Android mode

otherPopupRendererBool

Indicates that a separate renderer UIID/instance should be used to the list within the combo box popup

PackTouchMenuBool

Enables preferred sized packing of the touch menu (true by default), when set to false this allows manually determining the touch menu size using percentages

paintsTitleBarBool

Indicates that the StatusBar UIID should be added to the top of the form to space down the title area, as is the case on iOS 7+ where the status bar is painted on top of the UI

rightSideMenuImage

Overrides the default icon used for the right-side toolbar menu button

rightSideMenuPressImage

Optional pressed-state icon for the right-side toolbar menu button

popupCancelBodyBool

Indicates that a cancel button should appear within the combo box popup

PopupDialogArrowBool

Indicates whether the popup dialog has an arrow, notice that this constant will change if you change UIID of the popup dialog

PopupDialogArrowBottomImage

Image of the popup dialog arrow, notice that this constant will change if you change UIID of the popup dialog

PopupDialogArrowTopImage

Image of the popup dialog arrow, notice that this constant will change if you change UIID of the popup dialog

PopupDialogArrowLeftImage

Image of the popup dialog arrow, notice that this constant will change if you change UIID of the popup dialog

PopupDialogArrowRightImage

Image of the popup dialog arrow, notice that this constant will change if you change UIID of the popup dialog

popupNoTitleAddPaddingInt

Adds padding to a popup when no title is present

popupTitleBool

Indicates that a title should appear within the combo box popup

pullToRefreshImage

The arrow image used to draw the pullToRefresh animation

pureTouchBool

Indicates the pure touch mode

radioOppositeSideBool

Indicates the radio button should be drawn on the opposite side to the text and not next to the text

radioSelectedDisImage

Radio button image

radioSelectedImage

Radio button image

radioUnselectedDisImage

Radio button image

radioUnselectedImage

Radio button image

radioSelectedDisFocusImage

Radio button image

radioSelectedFocusImage

Radio button image

radioUnselectedDisFocusImage

Radio button image

radioUnselectedFocusImage

Radio button image

releaseRadiusInt

Indicates the distance from the button with dragging, in which the button should be released, defaults to 0

rendererShowsNumbersBool

Indicates whether renderers should render the entry number

reverseSoftButtonsBool

Swaps the softbutton positions

rightSideMenuImage

Same as sideMenuImage only for the right side, optional and defaults to sideMenuImage

rightSideMenuPressImage

Same as sideMenuPressImage only for the right side, optional and defaults to sideMenuPressImage

rubberBandCoefficientInt

Resistance coefficient for the iOS-style rubber-band compression used while the finger drags past a scroll edge. Value is interpreted as hundredths, so 55 means 0.55 (the iOS reference value). Larger values let the content be pulled farther for the same finger travel; smaller values make the content feel stiffer. Only active when iosScrollMotionBool is true. See Section 13.4.1, “Scroll motion tuning”

ScrollMotion

Selects the post-release flick model. DECAY (default) uses exponential velocity decay controlled by ScrollMotionTimeConstantInt and DecayMotionScaleFactorInt. Any other value (for example, FRICTION) falls back to the linear-friction model. See Section 13.4.1, “Scroll motion tuning”

ScrollMotionTimeConstantInt

Time constant in milliseconds for the exponential-decay flick model (active when ScrollMotion=DECAY). Default is 500. Smaller values stop the flick sooner

scrollVisibleBool

true/false default is platform dependent. Toggles whether the scroll bar is visible

showBackCommandOnTitleBool

Used by the Toolbar API to show whether the back button should appear on the title

shrinkPopupTitleBool

Indicates the title of the popup should be set to 0 if it’s missing

sideMenuAnimSpeedInt

The speed at which a sidemenu moves defaults to 300 milliseconds

sideMenuFoldedSwipeBool

Indicates the side menu could be opened via swiping

sideMenuImage

The image representing the side menu, three lines (Hamburger menu)

sideMenuPressImage

Optional pressed version of the sideMenuImage

sideMenuScrollVisibleBool

Indicates whether the scroll bar on the side menu should be visible or not, defaults to hidden

sideMenuShadowBool

Indicates whether the shadow for the side menu should be drawn

sideMenuShadowImage

The image used when drawing the shadow (a default is used if this isn’t supplied)

sideMenuSizeTabPortraitInt

The size of the side menu when expanded in a tablet in portrait mode

sideMenuSizePortraitInt

The size of the side menu when expanded in a phone in portrait mode

sideMenuSizeTabLandscapeInt

The size of the side menu when expanded in a tablet in landscape mode

sideMenuSizeLandscapeInt

The size of the side menu when expanded in a phone in landscape mode

sideMenuTensileDragBool

Enables/disables the tensile drag behavior within the opened side menu

sideSwipeActivationInt

Indicates the threshold in the side menu bar at which a swipe should trigger activation, defaults to 15 (percent)

sideSwipeSensitiveInt

Indicates the region of the screen that’s sensitive to side swipe in the side menu bar, defaults to 10 (percent)

sigButtonOKUIID

Defines the UIID applied to the confirmation button in the signature capture dialog (defaults to Button).

sigButtonResetUIID

Defines the UIID applied to the reset/clear button in the signature capture dialog (defaults to Button).

sigButtonCancelUIID

Defines the UIID applied to the cancel button in the signature capture dialog (defaults to Button).

slideDirection

Default slide transition settings

slideInDirBool

Default slide transition settings

slideOutDirBool

Default slide transition settings

sliderThumbImage

The thumb image that can appear on the sliders

snapGridBool

Snap to grid toggle

statusBarScrollsUpBool

Indicates that a tap on the status bar should scroll up the UI, only relevant in OSes where paintsTitleBarBool is true

switchButtonPadInt

Indicates the padding in the on/off switch, defaults to 16

switchMaskImage

Indicates the mask image used in iOS mode to draw on top of the switch

switchOnImage

Indicates the on image used in iOS mode to draw the on/off switch

switchOffImage

Indicates the off image used in iOS mode to draw the on/off switch

TabEnableAutoImageBool

Indicates images should be filled by default for tabs

TabSelectedImage

Default selected image for tabs (if TabEnableAutoImageBool=true)

TabUnselectedImage

Default unselected image for tabs (if TabEnableAutoImageBool=true)

tabPlacementInt

The placement of the tabs in the Tabs component: TOP = 0, LEFT = 1, BOTTOM = 2, RIGHT = 3

tabsSlideSpeedInt

The time of the animation that occurs (in milliseconds) between releasing a swiped tab and reaching the next tab. Defaults to 200

tabsFillRowsBool

Indicates if the tabs should fill the row using flow layout

tabsGridBool

Indicates whether tabs should use a grid layout thus forcing all tabs to have identical sizes

tabsOnTopBool

Indicates the tabs should be drawn on top of their content in a layered UI, this allows a tab to intrude into the content of the tabs

textCmpVAlignInt

The vertical alignment of the text component: TOP = 0, CENTER = 4, BOTTOM = 2

textComponentErrorColor

A hex RGB color which defaults to null in which case this has no effect. When defined this will change the color of the border and label to the given color to match the material design styling. This implements the red border underline in cases of error and the label text color change

textComponentOnTopBool

Toggles the on top mode which makes things look like they do on Android. This defaults to true on Android and false on other OSes. This can also be manipulated via the onTopMode(boolean) method in InputComponent, but the layout will only use the theme constant

textComponentAnimBool

toggles the animation mode which again can be manipulated by a method in InputComponent. If you want to keep the UI static without the floating hint effect set this to false. Notice this defaults to true only on Android

textComponentFieldUIID

sets the UIID of the text field to something other than TextField this is useful for platforms such as iOS where the look of the text field is different within the text component. This allows you to make the background of the text field transparent when it’s within the TextComponent and make it different from the regular text field

textFieldCursorColorInt

The color of the cursor as an integer (not hex)

tickerSpeedInt

The speed of label/button etc. (in milliseconds)

tintColor

The aarrggbb hex color to tint the screen when a dialog is shown

topMenuSizeTabPortraitInt

The size of the side menu when expanded and attached to the top in a tablet in portrait mode

topMenuSizePortraitInt

The size of the side menu when expanded and attached to the top in a phone in portrait mode

topMenuSizeTabLandscapeInt

The size of the side menu when expanded and attached to the top in a tablet in landscape mode

topMenuSizeLandscapeInt

The size of the side menu when expanded and attached to the top in a phone in landscape mode

touchCommandFillBool

Indicates how the touch menu should lay out the commands within

touchCommandFlowBool

Indicates how the touch menu should lay out the commands within

transitionSpeedInt

Indicates the default speed for transitions

treeFolderImage

Picture of a folder for the Tree class

treeFolderOpenImage

Picture of a folder expanded for the Tree class

treeNodeImage

Picture of a file node for the Tree class

tensileDragBool

Indicates that tensile drag should be enabled/disabled. This is set by platform themes

tensileSnapMinDurationInt

Minimum duration in milliseconds for the tensile snap-back animation when the finger is released past a scroll edge. Defaults to 400 when iosScrollMotionBool is true and 300 otherwise. The actual duration scales with the distance being snapped; this is only a floor

tensileSnapMotion

Motion curve used for the tensile snap-back. SPRING (the default when iosScrollMotionBool is true) applies a critically damped spring envelope that settles softly. DECELERATION (the default elsewhere) applies the legacy quadratic ease-out. See Section 13.4.1, “Scroll motion tuning”

13.4.1. Scroll motion tuning

Scroll feel is controlled by three stages, each tunable via theme constants:

  1. While the finger is dragging past an edge--when iosScrollMotionBool is true, the over-edge distance is compressed by the iOS UIScrollView rubber-band function c * d * dim / (c * d + dim), where d is the raw finger displacement past the edge, dim is the viewport dimension, and c is rubberBandCoefficientInt / 100 (default 0.55). The result is that the content feels heavier the farther you pull it, asymptotically approaching dim. When iosScrollMotionBool is false, the legacy behavior is used: a linear 1:1 follow clamped to tensileLength.

  2. After the finger is lifted, while there is still velocity--the ScrollMotion constant selects the model. DECAY (default) uses exponential velocity decay with a time constant of ScrollMotionTimeConstantInt milliseconds and a distance scale of DecayMotionScaleFactorInt. Any other value (for example, FRICTION) falls back to linear friction.

  3. When the scroll comes to rest past an edge--a tensile snap-back animates the content back to the edge. tensileSnapMotion picks the curve: SPRING is a critically damped envelope (iOS-like soft settle) and DECELERATION is the legacy quadratic ease-out. The duration scales with distance but never falls below tensileSnapMinDurationInt milliseconds.

    iosScrollMotionBool is the single switch that flips stage 1 and the default of stage 3 to the iOS-matching behavior. It defaults to true on the iOS platform only. Every individual constant can still be overridden regardless of this switch, so you can, for example, enable the nonlinear rubber-band globally (iosScrollMotionBool=true) while keeping the legacy quadratic snap-back (tensileSnapMotion=DECELERATION).

Tuning suggestions
  • To make scrolling feel stiffer on iOS (less pull), reduce rubberBandCoefficientInt from 55 toward 35.

  • To make flicks stop sooner, reduce ScrollMotionTimeConstantInt (default 500) or DecayMotionScaleFactorInt (default 950).

  • To enable iOS-style physics on Android or desktop for consistency, set iosScrollMotionBool=true explicitly.

  • To disable iOS physics on the simulator while debugging, set iosScrollMotionBool=false in your development theme.

Dynamic Theme Swapping & Theme Constants

Once a theme constant is set by a theme, it isn’t removed on a refresh when replacing the theme.

For example, if one would set the comboImage constant to a specific value in theme A and then switch to theme B, that doesn’t define the comboImage, the original theme A comboImage might remain!

The reason for this is simple: when extracting the constant values, components keep the values in cache locally and just don’t track the change in value. Furthermore, since the components allow manually setting values, it’s impractical for them to track whether a value was set by a constant or explicitly by the user.

The solution for this is to either manually reset undesired values before replacing a theme (for example, for the case, above by calling the default look and feel method for setting the combo image with a null value), or defining a constant value to replace the existing value.

13.5. Native theming

Codename One uses a theme constant called includeNativeBool, when that constant is set to true Codename One starts by loading the native theme first and then applying all the theme settings. This means your theme "derives" the style of the native theme first, like the cascading effect of CSS. Internally this is what the theme layering section covered.

By avoiding this flag you can create themes that look EXACTLY the same on all platforms.

If you avoid the native theming you might be on your own. A few small device oddities such as the iOS status bar are abstracted by native theming. Without it you will need to do everything from scratch

You can simulate different OS platforms by using the native theme menu option

The native theme menu option
Figure 185. The native theme menu option

Developers can pick the platform of their liking and see how the theme will appear in that particular platform by selecting it and having the preview update on the fly.

13.6. Under the hood of the theme engine

To truly understand a theme you need to understand what it is. Internally a theme is a Hashtable key/value pair between UIID based keys and their respective values. For example: the key:

return "Button.fgColor=ffffff";

Will set the foreground color of the Button UIID to white.

When a Codename One Component is instantiated it requests a Style object from the UIManager class. The Style object is based on the settings within the theme and can be modified through code or by using the theme.

You can replace the theme dynamically in runtime and refresh the styles assigned to the various components using the refreshTheme() method.

It’s a common mistake to invoke refreshTheme() without actually changing the theme. You see developers doing it when all they need is a repaint() or revalidate(). Since refreshTheme() is ** expensive recommend that you don’t use it unless you need to…​

A theme Hashtable key comprises:

[UIID.][type#]attribute

The UIID, corresponds to the component’s UIID for example: Button, CheckBox etc. It’s optional and may be omitted to address the global default style.

The type is omitted for the default unselected type, and may be one of sel (selected type), dis (disabled type) or press (pressed type). The attribute should be one of:

  • derive - the value for this attribute should be a string representing the base component.

  • bgColor - represents the background color for the component, if applicable, in a web hex string format RRGGBB for example: ff0000 for red.

  • fgColor - represents the foreground color, if applicable.

  • border - an instance of the border class, used to display the border for the component.

  • bgImage - an Image object used in the background of a component.

  • transparency - a String containing a number between 0-255 representing the alpha value for the background. This applies to the bgColor.

  • margin - the margin of the component as a String containing 4 comma separated numbers for top, bottom, left, right.

  • padding - the padding of the component, it has an identical format to the margin attribute.

  • font - A Font object instance.

  • alignment - an Integer object containing the LEFT/RIGHT/CENTER constant values defined in Component.

  • textDecoration - an Integer value containing one of the TEXT_DECORATION_* constant values defined in Style.

  • backgroundType - a Byte object containing one of the constants for the background type defined in Style under BACKGROUND_*.

  • backgroundGradient - contains an Object array containing 2 integers for the colors of the gradient. If the gradient is radial it contains 3 floating points defining the x, y & size of the gradient.

To set the foreground color of a selected button to red, a theme will define a property like:

Button.sel#fgColor=ff0000

This information is useful for understanding how things work within Codename One, but it can also be useful in runtime.

For example: to increase the size of all fonts in the application, you can do something like:

Hashtable h = new Hashtable();
h.put("font", largeFont);
UIManager.getInstance().addThemeProps(h);
Display.getInstance().getCurrent().refreshTheme();

13.7. Understanding images and Multi-Images

This section provides a high-level overview of images. You dive deeper into the various types of images in the graphics section.

When working with a theme, you often use images for borders or backgrounds. You also use images within the GUI for various purposes and most such images will be extracted from the resource file.

Adding a standard JPEG/PNG image to the resource file is straight forward, and the resulting image can be viewed within the images section. For example, due to the wide difference between device types, an image that would be appropriate in size for an iPhone 3gs would not be appropriate in size for a Nexus device or an iPhone 4 (but perhaps, it will be right for iPad 1 and iPad 2).

The density of the devices varies and Codename One tries to simplify the process by unifying everything into one set of values to show density. For simplicity’s sake, density is sometimes expressed in pixels, but it’s mapped internally to actual screen measurements where possible.

A multi-image is an image that has many varieties for different densities, and thus looks sharp in all the densities. Since scaling on the device can’t interpolate the data (due to performance considerations), significant scaling on the device becomes impractical. For example, a multi-image will provide the "right" resolution image for the given device type.

From the programming perspective this is seamless, a developer accesses one image and has no ability to access the images in the different resolutions. Within the designer, but, you can explicitly define images for many resolutions and perform high quality scaling so the "right" image is available.

You can use two basic methods to add a multi-image: quick add and standard add.

Both methods rely on understanding the source resolution of the image, for example: if you have an icon that you expect to be 128×128 pixels on iPhone 4, 102×102 on nexus one and 64×64 on iPhone 3gs. You can provide the source image as the 128 pixel image and perform a quick add option while picking the # High# density option.

This will show to the algorithm that your source image is designed for the " high" density and it will scale for the rest of the densities accordingly.

This relies on the common use case of asking your designer to design for one high end device (for example: iPhone X) then you can take the resources and add them as "HD" resources. They will automatically adapt to the lower resolutions

Or, you can use the standard add multi-image dialog and set it like this:

Multi-image resolution dialog

Notice that you selected the square image option, essentially eliminating the height option. Setting values to 0 prevents the system from generating a multi-image entry for that resolution, which will mean a device in that category will fall on the closest alternative.

The percentage value will change the entire column, and it means the percentage of the screen. For example: You know the icon is 128 for the high resolution, you can move the percentage until you reach something close to 128 in the " High" row and the other rows will represent a size that should be pretty close in physical size to the 128 figure.

At runtime, you can always find the host device’s approximate pixel density using the Display.getDeviceDensity() method. This will return one of:

Table 7. Densities

Constant

Density

Example Device

Display.DENSITY_VERY_LOW

~ 88 ppi

Display.DENSITY_LOW

~ 120 ppi

Android ldpi devices

Display.DENSITY_MEDIUM

~ 160 ppi

iPhone 3GS, iPad, Android mdpi devices

Display.DENSITY_HIGH

~ 240 ppi

Android hdpi devices

Display.DENSITY_VERY_HIGH

~ 320 ppi

iPhone 4, iPad Air 2, Android xhdpi devices

Display.DENSITY_HD

~ 540 ppi

iPhone 6+, Android xxhdpi devices

Display.DENSITY_560

~ 750 ppi

Android xxxhdpi devices

Density.DENSITY_2HD

~ 1000 ppi

Density.DENSITY_4K

~ 1250ppi

13.8. Use millimeters for Padding/Margin and font sizes

When configuring your styles, you should rarely use "Pixels" as the unit for padding, margins, font size, and border thickness because the results will be inconsistent on different densities. Instead, you should use millimeters for all non-zero units of measurement.

As you now understand the complexities of DPI it should be clear why this is important.

13.8.1. Fractions of millimeters

Sometimes millimeters don’t give you enough precision for what you want to do. The designer allows you to specify integer values for most units. For example, you can achieve more precise results when working directly in Java. The Display.convertToPixels() method will allow you to convert millimeters (or DIPS) to pixels. It also takes an integer input, but you can use it to get a multiplier that you can then use to convert any millimeter value you want into pixels.

For example:

double pixelsPerMM = ((double) Display.getInstance().convertToPixels(10, true)) / 10.0;

And now you can set the padding on an element to 1.5mm. For example:

myButton.getAllStyles().setPaddingUnit(Style.UNIT_TYPE_PIXELS);
int pixels = (int) (1.5 * pixelsPerMM);
myButton.getAllStyles().setPadding(pixels, pixels, pixels, pixels);

13.9. Creating a great looking side menu

Side Menu final result
Figure 186. Side Menu final result

A side menu is a crucial piece of an elegant application. You will explain how one creates a simple side menu that’s elegant, portable and easy to build. This is a good "starting point" side menu from which you can build more elaborate designs.

To get this result you will start from a native theme and a bare-bones application to keep things simple.

The code for the side menu is this:

Form hi = new Form("Hi World");

Toolbar tb = hi.getToolbar();
Image icon = theme.getImage("icon.png"); (1)
Container topBar = BorderLayout.east(new Label(icon));
topBar.add(BorderLayout.SOUTH, new Label("Cool App Tagline...", "SidemenuTagline")); (2)
topBar.setUIID("SideCommand");
tb.addComponentToSideMenu(topBar);

tb.addMaterialCommandToSideMenu("Home", FontImage.MATERIAL_HOME, e -> {}); (3)
tb.addMaterialCommandToSideMenu("Website", FontImage.MATERIAL_WEB, e -> {});
tb.addMaterialCommandToSideMenu("Settings", FontImage.MATERIAL_SETTINGS, e -> {});
tb.addMaterialCommandToSideMenu("About", FontImage.MATERIAL_INFO, e -> {});

hi.addComponent(new Label("Hi World"));
hi.show();
1This is the icon which was used instead of a logo it appears in the top right of the side menu
2This is the top bar containing the tagline and the icon it’s styled as if it’s a command but you can put anything here for example: an image etc.
3The commands are added as usual to the side menu with no styling or functionality, the entire look is determined by the theme

Next you will open the designer tool to style the UI

Open the side menu so you will get the right values in the combo box on add
Figure 187. Open the side menu so you will get the right values in the combo box on add

Now when you press Add the side menu entries will appear in the combo box (you can type them but this is more convenient). You will start with the SideNavigationPanel style:

The SideNavigationPanel has an opaque white background
Figure 188. The SideNavigationPanel has an opaque white background

The SideCommand style is a bit more elaborate, you start with a white foreground and an opaque bluish/purple color:

The SideCommand has a white foreground and opaque bluish background
Figure 189. The SideCommand has a white foreground and opaque bluish background

You will set padding to 3 millimeters which provides everything a good feel and spacing. This is important for finger touch sensitivity.

Padding is 3mm so it will feel spacious and touch friendly
Figure 190. Padding is 3mm so it will feel spacious and touch friendly

You will set margin to 0 except for the bottom one pixel which will leave a nice white line by showing off the background. This means the commands will have a space between them and the white style you gave to the SideNavigationPanel will appear through that space.

Margin is 0 except for a thin line below each command
Figure 191. Margin is 0 except for a thin line below each command

Setting the border to empty is crucial!

The iOS version of the side command inherits a border style so you must "remove" it by defining a different border in this case an empty border. Since borders take precedence over color this would have prevented the color changes you made from appearing.

Border must be defined as Empty
Figure 192. Border must be defined as Empty

Next you need to pick a good looking font and make sure it’s large enough. You use millimeters size it for all OS’s and override the derived text decoration which has a value in the iOS native theme so it can impact the final look.

Pick a good looking font for the side command
Figure 193. Pick a good looking font for the side command

Next you need to move to the selected tab and add a new side command entry that derives from the unselected version. You will pick a new color that’s slightly deeper and will make the selected style appear selected. You will also copy and paste this selected style to the pressed style.

Selected & Pressed SideCommand
Figure 194. Selected & Pressed SideCommand
Color for the Selected/Pressed SideCommand
Figure 195. Color for the Selected/Pressed SideCommand

The SidemenuTagline is a SideCommand style that was slightly adapted. You will remove the padding and margin because the whole section is wrapped in a side command and you don’t want double padding. You will leave 1mm padding at the top for a bit of spacing from the logo.

Padding of the SidemenuTagline
Figure 196. Padding of the SidemenuTagline

You will also update the font to a smaller size and italic styling so it will feel like a tagline.

Font for the SideMenuTagline is slightly smaller and italic
Figure 197. Font for the SideMenuTagline is slightly smaller and italic

The last change for the theme is for the StatusBarSideMenu UIID which is a spacing on the top of the sidemenu. This spacing is there for iOS devices which render the clock/battery/reception symbols on top of the app. You will set the padding to 0.

StatusBarSideMenu padding for the top of the side menu
Figure 198. StatusBarSideMenu padding for the top of the side menu

Finally, you will add the icon image (or a logo if you have it) into the theme as a multi image so you can use it within the side menu as a good looking logo. A large icon image works as a 2HD multi-image but you can use many strategies to get a fitting image for this spot.

Rounded images work well here, you can round images dynamically using masking

These steps produce the UI above as a side menu, they might seem like a long set of steps but each step is pretty simple as you walk through each one. This does show off the versatility and power of Codename One as a change to one step can create a radically different UI design.

13.10. From a design to a theme

A design usually arrives as a mock-up from someone who doesn’t write apps, and the work is the same whichever tool it came out of: decide which components and layout managers reproduce the structure, export the images you need at the densities you need, and then style it.

Sign up form design
Figure 199. Sign up form design

The structure is the part worth doing on paper first. Nesting containers to match the design, rather than positioning things absolutely, is what keeps it working across screen sizes and platforms:

Component hierarchy and layouts
Figure 200. Component hierarchy and layouts

Export the artwork the design needs before writing the code. This one needs three pieces: the header background, and the camera icon with the round button it sits on.

The header background
Figure 201. The header background
The camera icon
Figure 202. The camera icon
The button behind it
Figure 203. The button behind it

Only images a stylesheet names reach the compiled theme, and the camera pieces are fetched by name at runtime rather than used as a background, so the theme below carries a selector that exists purely to import them. Each is declared at its source density so the compiler produces a multi-image and every screen gets an appropriately sized file, which is what theme.getImage then returns.

That hierarchy is ordinary component code. The code below builds the form, starting with a separator the design uses between fields:

private Label createSeparator() {
    Label sep = new Label();
    sep.setUIID("Separator");
    // the separator line  is implemented in the theme using padding and background color, by default labels
    // are hidden when they have no content, this method disables that behavior
    sep.setShowEvenIfBlank(true);
    return sep;
}
if (current != null) {
    current.show();
    return;
}
// The toolbar uses the layered mode so it resides on top of the background image, the theme makes
// it transparent so we will see the image below it, we use border layout to place the background image on
// top and the "Get started" button in the south
Form psdTutorial = new Form("Signup", new BorderLayout());
Toolbar tb = new Toolbar(true);
psdTutorial.setToolbar(tb);

// we create 4mm material arrow images for the back button and the Get started button
Style iconStyle = psdTutorial.getUIManager().getComponentStyle("Title");
FontImage leftArrow = FontImage.createMaterial(FontImage.MATERIAL_ARROW_BACK, iconStyle, 4);
FontImage rightArrow = FontImage.createMaterial(FontImage.MATERIAL_ARROW_FORWARD, iconStyle, 4);

// we place the back and done commands in the toolbar, we need to change UIID of the "Done" command
// so we can color it in Red
tb.addCommandToLeftBar("", leftArrow, (e) -> Log.p("Back pressed"));
Command doneCommand = tb.addCommandToRightBar("Done", null, (e) -> Log.p("Done pressed"));
tb.findCommandComponent(doneCommand).setUIID("RedCommand");

// The camera button is comprised of 3 pieces. A label containing the image and the transparent button
// with the camera icon on top. This is all wrapped in the title container where the title background image
// is placed using the theme. We chose to use a Label rather than a background using the cameraLayer so
// the label will preserve the original size of the image without scaling it and take up the space it needs
Button cameraButton = new Button(theme.getImage("camera.png"));
Container cameraLayer = LayeredLayout.encloseIn(
        new Label(theme.getImage("camera-button.png")),
        cameraButton);
cameraButton.setUIID("CameraButton");
Container titleContainer = Container.encloseIn(
        new BorderLayout(BorderLayout.CENTER_BEHAVIOR_CENTER),
        cameraLayer, BorderLayout.CENTER);
titleContainer.setUIID("TitleContainer");

TextField firstName = new TextField("", "First Name");
TextField lastName = new TextField("", "Last Name");
TextField email = new TextField("", "Email Address", 20, TextField.EMAILADDR);
TextField password = new TextField("", "Choose a Password", 20, TextField.PASSWORD);
TextField phone = new TextField("", "Phone Number", 20, TextField.PHONENUMBER);
Label phonePrefix = new Label("+1");
phonePrefix.setUIID("TextField");

// The phone and full name have vertical separators, we use two table layouts to arrange them correctly
// so the vertical separator will be in the right place
TableLayout fullNameLayout = new TableLayout(1, 3);
Container fullName = new Container(fullNameLayout);
fullName.add(fullNameLayout.createConstraint().widthPercentage(49), firstName).
        add(fullNameLayout.createConstraint().widthPercentage(1), createSeparator()).
        add(fullNameLayout.createConstraint().widthPercentage(50), lastName);
Container fullPhone = TableLayout.encloseIn(3, phonePrefix, createSeparator(), phone);

// The button in the south portion needs the arrow icon to be on the right side so we place the text on the left
Button southButton = new Button("Get started", rightArrow);
southButton.setTextPosition(Component.LEFT);
southButton.setUIID("SouthButton");

// we add the components and the separators the center portion contains all of the elements in a box
// Y container which we allow to scroll. BorderLayout Containers implicitly disable scrolling
Container by = BoxLayout.encloseY(
        fullName,
        createSeparator(),
        email,
        createSeparator(),
        password,
        createSeparator(),
        fullPhone,
        createSeparator()
);
by.setScrollableY(true);
psdTutorial.add(BorderLayout.NORTH, titleContainer).
        add(BorderLayout.SOUTH, southButton).
        add(BorderLayout.CENTER, by);

psdTutorial.show();

Style it with Chapter 15, CSS. Ten UIIDs cover this design, and every value in it maps to an ordinary CSS property: the scaled header image is cn1-background-type, a UIID that only varies a color inherits the rest with cn1-derive, and the pressed and selected states are the .pressed and .selected classes rather than separate definitions. Sizes are in millimetres so they hold their physical size across densities.

TitleContainer {
    background-image: url(images/background.jpg);
    cn1-background-type: cn1-image-scaled-fill;
    cn1-source-dpi: 320;
    padding: 8mm 3mm 2mm 3mm;
    margin: 0;
}

/* The camera artwork is fetched by name at runtime rather than set as a
   background, so it needs a selector to get it into the theme at all. This one
   is never applied to anything; naming the images here is what bundles them. */
PsdImages {
    background-image: url(images/camera.png),
        url(images/camera-button.png);
    cn1-source-dpi: 320;
}

Title {
    background-color: transparent;
    color: #ffffff;
    font-family: "native:MainThin";
    font-size: 3.5mm;
    padding: 1mm;
    margin: 0;
    border: none;
}

TitleCommand {
    cn1-derive: Title;
}

RedCommand, RedCommand.pressed, RedCommand.selected {
    cn1-derive: Title;
    color: #f73267;
    font-family: "native:MainLight";
    font-size: 3mm;
}

TitleArea {
    background-color: transparent;
    padding: 0;
    margin: 0;
    border: none;
}

TextField, TextField.selected {
    background-color: #ffffff;
    font-family: "native:MainLight";
    font-size: 2mm;
    padding: 2mm;
    margin: 0;
    border: none;
}

TextHint {
    cn1-derive: TextField;
    color: #4d606f;
}

SouthButton, SouthButton.pressed, SouthButton.selected {
    background-color: #f73267;
    color: #ffffff;
    text-align: center;
    font-family: "native:MainThin";
    font-size: 3mm;
    padding: 2mm 1mm 2mm 1mm;
    margin: 0;
}

CameraButton, CameraButton.pressed, CameraButton.selected {
    background-color: transparent;
    text-align: center;
    padding: 3mm 1mm 1mm 1mm;
    margin: 1mm;
}

/* The separator is an empty Label, so the line is the style: a background
   colour and just enough padding to give it height. */
Separator {
    background-color: #f5f5f5;
    padding: 0.2mm 0 0 0;
    margin: 1mm 0 1mm 0;
    border: none;
}

That’s the whole theme. With it in place the same code produces the design:

The result in the simulator
Figure 204. The result in the simulator

14. Native modern themes

Codename One ships an iOS Modern (liquid-glass) theme and an Android Material 3 theme that you can opt your app into with a single build hint. Both are fully overridable - their color palettes, type ramps, and state-specific styles are all reachable from your own theme.css and from runtime API.

The legacy iOS 7 (iOS) and Holo Light (Android) themes remain the default so existing apps see no behavior change.

14.1. Inheriting OS colors and fonts

Modern native themes can optionally follow the user’s operating-system settings. Colors and fonts are independent opt-ins; both are off by default. Enable them in an application theme that includes the native theme:

#Constants {
    includeNativeBool: true;
    useNativeColorsBool: true;
    useNativeFontsBool: true;
    useLargerTextScaleBool: true;
}

Alternatively, configure the UIManager before loading the application theme:

UIManager ui = UIManager.getInstance();
ui.setUseNativeColors(true);
ui.setUseNativeFonts(true);
ui.setUseLargerTextScale(true);

An explicit theme constant takes precedence over its Java setter when the theme is built. After changing a setter on an already loaded theme, call UIManager.refreshTheme() and refresh/revalidate the visible form. A theme constant set to false must also be changed or removed before a setter can enable that option.

Color inheritance feeds the theme’s semantic palette bindings. Font inheritance uses the normal OS UI font family and size while retaining the native theme’s relative heading sizes and styles. Explicit application style properties and palette constants override these inherited defaults. Bundled application fonts and icon fonts aren’t replaced. Legacy hand-authored native themes don’t carry the inheritance metadata and retain their existing appearance.

The existing larger-text accessibility option remains independent. It scales eligible fonts after OS font inheritance, so enabling both doesn’t apply the OS text-size multiplier twice. Existing dark-mode selection remains independent too.

Settings changes and application activation trigger a coalesced theme refresh. The framework preserves application overrides, refreshes visible styles and layout, and restores bundled defaults when settings become unavailable or an inheritance option is disabled. Unsupported settings fall back individually.

PortSettings source

Windows native

Windows UISettings accent/selection palette, high-contrast system colors, and the nonclient message font. Normal Fluent surfaces retain their bundled colors where no corresponding OS value is exposed.

macOS native

AppKit semantic colors and normal system UI typography.

Linux native

The active GTK palette and GTK font setting. Only available named colors are inherited; the opposite appearance retains its bundled palette.

Android

Platform normal text appearance and, on Android 12 or newer, public dynamic-color resources. Older versions retain the bundled palette.

iOS

UIKit semantic colors on iOS 13 or newer and normal system UI typography.

JavaSE

Available AWT desktop properties; the simulator uses explicit simulated settings rather than the host’s palette.

In the simulator, use Simulate > OS Theme Settings to choose an accent and UI font or reset the simulated settings. These controls exercise the app’s opt-ins; they don’t enable inheritance for the app. Tests can supply a deterministic snapshot with JavaSEPort.setSimulatorNativeThemeSettings(…​).

14.2. Selecting a theme

Three build hints control the platform native theme. None of them are required; with all three unset every app continues to load the legacy theme it always did.

HintValuesDescription

ios.themeMode

auto (default)
modern / liquid
ios7 / flat
legacy / iphone

auto and ios7 both load the iOS 7 flat theme. modern / liquid opts in to the modern theme generated from native-themes/ios-modern/theme.css. legacy / iphone loads the pre-iOS 7 theme. The auto → modern flip is planned for a future release.

and.themeMode

auto / modern / material
hololight (legacy default)
legacy

auto and modern / material opt in to the modern theme generated from native-themes/android-material/theme.css. hololight is Android Holo Light (the pre-modern default for existing apps). legacy is the pre-Holo theme. The legacy alias cn1.androidTheme is still honored for back-compat.

ios.themeGeneration

26 (default)
27

Which iOS design generation the modern theme targets. Consulted only when ios.themeMode resolves to modern; the iOS 7 and pre-flat looks have one generation each and ignore it. Unset is 26, so an existing project keeps the theme it has. 27 is a real design change rather than a version bump — 31 of the 68 native reference tiles differ, concentrated in the Liquid Glass surfaces and the bar and button chrome built on them, while the classic form controls are identical.

nativeTheme

modern / legacy / custom

Cross-platform shortcut that sets both ios.themeMode and and.themeMode together. modern = liquid glass + Material 3, legacy = iOS 7 + Holo Light, custom disables the framework theme entirely so your own theme.css owns the base layer. The legacy alias cn1.nativeTheme is still honored for back-compat.

The legacy and.hololight=true hint still works and maps to and.themeMode=hololight.

14.3. Desktop themes

Windows, macOS and GNOME have their own native themes, selected with desktop.themeMode on the JavaSE desktop build and with macos.themeMode on the native macOS port.

HintValuesDescription

desktop.themeMode

legacy (default)
auto / native
fluent / aqua / adwaita
custom

Unset is legacy: an existing desktop project keeps the theme it has always loaded, so upgrading the framework doesn’t restyle an application nobody asked to restyle. auto and native opt in and both mean "whatever this machine is" — Windows 11 Fluent, macOS Aqua or GNOME Adwaita — which is the only sensible reading on desktop, where one binary runs on all three. Naming a theme outright is what a build that wants one look everywhere asks for. custom installs no framework theme so your own theme.css is the only one.

macos.themeMode

aqua (default) / native
modern / liquid
ios7 / flat

The native macOS port. When neither this hint nor the shared nativeTheme hint is set, the default is Aqua. aqua and native select the macOS theme; modern / liquid select the iOS Liquid Glass theme. An explicit shared hint still applies when macos.themeMode is unset: nativeTheme=modern selects the modern theme, while nativeTheme=legacy selects ios7.

The three desktop themes are generated from native-themes/windows-fluent, native-themes/macos-aqua and native-themes/gnome-adwaita. They’re separate files rather than one file with per-platform blocks, and they’re held to the same surface by DesktopNativeThemeParityTest: the same UIIDs, the same #Constants, and a $Dark counterpart for everything that paints a color. That matters to you and not only to the themes — an application retunes a parent theme by redeclaring #Constants, so one vocabulary means the same CSS retunes all three platforms rather than only the one it was written against.

14.3.1. One application on all three

The Codename One Settings tool is built this way. Its main class declares @DesktopBuild(themeMode = "native"), so it opens in Fluent on Windows, Aqua on macOS and Adwaita on Linux, and its own theme.css never names a color the platform owns:

  • Each control derives from the native UIID it’s a variant of — cn1-derive: TextField for a field, RaisedButton for the primary action, GroupBox for a card — so borders, fills and focus rings are the platform’s.

  • Surfaces and text use the palette variables every desktop theme declares, such as var(--window-bg-color) and var(--text-secondary-color). At runtime the installed theme’s values replace the ones the compiler baked in.

  • Only the brand is fixed: the tool redeclares --accent-color in its #Constants, and the Save button keeps its own green.

The Settings tool under Windows 11 Fluent

Settings under Fluent in light mode Settings under Fluent in dark mode

The Settings tool under macOS Aqua

Settings under Aqua in light mode Settings under Aqua in dark mode

The Settings tool under GNOME Adwaita

Settings under Adwaita in light mode Settings under Adwaita in dark mode

All six images were captured on a Mac, not on Windows or Linux. Colors, borders and spacing come from the theme and are the same on every host, but each theme draws text in its platform’s own typeface — Segoe UI Variable, SF or Cantarell — and the Fluent and Adwaita images fall back to the Mac’s system font. On a real Windows or Linux desktop the text looks different.

In a web browser, nativeTheme=native selects the desktop theme of the browser’s operating system, and the javascript.titleBar build hint decides whether the title and the commands stay in the Toolbar or move to an HTML title bar and menu bar. See Changing the Native Theme.

To preview a desktop theme in the simulator, pick the Desktop skin and choose the theme from the Native Theme menu. With Auto selected the Desktop skin follows desktop.themeMode, which is what the packaged application does.

14.3.2. Hover

Desktop themes carry a state mobile ones have no use for. A .hover rule styles a component while the pointer is over it:

Button.hover {
    background-color: #f6f6f6;
}

It compiles to hover# entries the same way .pressed compiles to press#, and Component.getHoverStyle() returns null when the theme declares none — so a component with no hover rule keeps its normal style rather than falling back to a blank one.

Hover has no meaning on a touch device and isn’t delivered there. On desktop it’s driven by real pointer motion: the JavaSE, Windows and Linux ports all report motion with no button held, which is what Form.pointerHover resolves against.

macOS is the exception, and the reason belongs to AppKit rather than to Codename One. AppKit draws no rollover state for buttons, fields, sliders, switches or popup buttons, so the Aqua theme leaves hover equal to normal. The captured native reference says the same, and the fidelity gate holds it there.

14.4. Light and dark mode

Both modern themes ship a dark variant via a @media (prefers-color-scheme: dark) block in their CSS. The CSS compiler rewrites each rule inside that block into a $Dark<UIID> style entry; UIManager picks them up whenever CN.isDarkMode() == true.

One form under Android Material, rendered in each appearance

Light appearance Dark appearance

Nothing in that form is styled by hand, so the difference between the two pictures is entirely what the theme’s dark entries supply.

Display.getInstance().setDarkMode(Boolean) controls the active appearance:

  • null (the default) - follow the device. iOS reads UITraitCollection, Android reads Configuration.uiMode. When the user toggles light / dark in system settings the app reflects the change. A JavaSE desktop application reads the system appearance once, at launch: AppsUseLightTheme on Windows, AppleInterfaceStyle on macOS and the GNOME color-scheme setting on Linux. In the simulator the Dark/Light Mode menu decides instead.

  • true / false - force the app into one mode regardless of system setting (useful for theme-preview screens or accessibility toggles).

14.5. Material 3 color palette (Android)

The Android Material 3 theme is built around the Material Design 3 "baseline" palette. Each color is referenced from one or more UIIDs; if you change a color the change ripples to every UIID that inherits from it.

RoleLightDarkUsed by

primary

#6750a4

#d0bcff

Default Button fill, BackCommand / TitleCommand text, SelectedTab text color.

on-primary

#ffffff

#381e72

Text printed on top of the primary fill.

primary-container

#eaddff

#4f378b

RaisedButton (the elevated tone), Button.pressed and MultiButton.pressed highlight, FloatingActionButton.

surface

#fef7ff

#141218

Form, ContentPane, Toolbar, TitleArea, List, Tabs, SideNavigationPanel. The "page" color.

on-surface

#1d1b20

#e6e0e9

Body text on surfaces (Label, Title, MultiLine1).

surface-variant

#e7e0ec

#49454f

Track of the off-state Switch.

on-surface-variant

#49454f

#cac4d0

Secondary text (SecondaryLabel, MultiLine2, UnselectedTab text).

surface-container

#f3edf7

#211f26

TextField / TextArea filled background.

surface-container-highest

#e6e0e9

#211f26

Dialog, PopupContent. Sits one elevation step above surface so dialogs read as raised cards.

outline / outline-variant

#79747e / #cac4d0

#938f99 / #49454f

Separator, TertiaryLabel, disabled-text muting.

state-pressed

#d0bcff

#4f378b

All .pressed overrides on Button-family UIIDs.

state-disabled / on-disabled

#e0dce4 / #a5a0ab

#2b2930 / #5c5967

All .disabled overrides.

To rebrand the app, override the color at the role level rather than touching every UIID. The example below layers a teal palette on top from your app’s own theme.css; if you want the same rebrand to apply at runtime (for in-app accent toggles, branded flavours, A/B tests) without recompiling the theme, see Runtime accent palette override further down - a single addThemeProps({"@accent-color": …​}) call retunes every accent-bearing UIID at once:

#Constants {
    includeNativeBool: true;
    darkModeBool: true;
}

/* Touch the four UIIDs that paint with primary / primary-container.
   The role-as-rule pattern keeps brand changes localised. */
Button { background-color: #00796b; }
Button.pressed { background-color: #4db6ac; color: #00251a; }
RaisedButton { background-color: #b2dfdb; color: #00251a; }
RaisedButton.pressed { background-color: #80cbc4; }
SelectedTab { color: #00796b; }
BackCommand { color: #00796b; }
TitleCommand { color: #00796b; }

14.6. iOS modern color palette

The iOS modern theme follows Apple’s system palette.

RoleLightDarkUsed by

accent

#007aff

#0a84ff

Button text, RaisedButton fill, SelectedTab text, BackCommand / TitleCommand, FloatingActionButton.

accent-pressed

#0064d1

#64b1ff

All .pressed accent overrides.

accent-disabled

#b3d4ff

#004a99

All .disabled accent overrides.

surface

#ffffff

#000000

Pure surface used by Toolbar, TitleArea, Title, List.

surface-grouped

#f2f2f7

#1c1c1e

Form / ContentPane (the iOS "grouped" form bg). Dialog / Tabs use this color at reduced opacity for the liquid-glass look.

surface-tertiary

#e5e5ea

#2c2c2e

Button.pressed highlight, elevated dark Dialog surface.

text-primary

#000000

#ffffff

Label, MultiLine1, Title, primary body text.

text-secondary

#3c3c43

#ebebf5

SecondaryLabel, MultiLine2, unselected tabs.

text-tertiary / text-disabled

#8e8e93 / #c7c7cc

#8e8e93 / #48484a

TertiaryLabel, MultiLine3-4, disabled-state text.

separator

#c6c6c8

#38383a

Separator.

success

#34c759

#30d158

Switch.selected, OnOffSwitch.selected.

To layer a brand color on top, override RaisedButton and accent-driven UIIDs the same way as Android above. The color names match Apple’s UIColor.systemBlue etc. So you can mirror the SF Symbols semantics if you want.

14.7. Accent palette override

Both shipped native themes expose their accent palette as named constants you can retune from your app’s own theme.css (or, for dynamic theming, at runtime). The CSS source uses var(--accent-color, fallback) references so the fallback ships as the baked-in default (the .res file loads fine with no override) and the compiler additionally emits a @cn1-bind:<UIID>.<key>=accent-color constant alongside each affected style key. When the resolved theme constants pick up a new @accent-color value (whether from your CSS or via runtime UIManager.addThemeProps), the framework fans the override out to every bound UIID at once - no per-UIID rule duplication, no theme recompile.

Table 8. iOS modern (native-themes/ios-modern/theme.css)
ConstantLight defaultDark defaultDrives

@accent-color

#007aff

(see @accent-color-dark)

Button.fgColor, RaisedButton.bgColor, CheckBox.selected, RadioButton.selected, OnOffSwitch.fgColor, BackCommand, TitleCommand, FloatingActionButton.bgColor.

@accent-color-dark

n/a

#0a84ff

$Dark<UIID> counterparts of the above.

@accent-pressed-color / @accent-pressed-color-dark

#0064d1

#64b1ff

All .press#fg/bgColor accent overrides.

@accent-disabled-color / @accent-disabled-color-dark

#b3d4ff

#004a99

All .dis#fg/bgColor accent overrides.

@accent-on-color

#ffffff

(same)

Text color painted on top of the accent fill.

Table 9. Android Material 3 (native-themes/android-material/theme.css)
ConstantLight defaultDark defaultMaterial 3 token

@accent-color / @accent-color-dark

#6750a4

#d0bcff

primary

@accent-on-color / @accent-on-color-dark

#ffffff

#381e72

on-primary

@accent-container-color / @accent-container-color-dark

#eaddff

#4f378b

primary-container

@accent-on-container-color / @accent-on-container-color-dark

#21005d

#eaddff

on-primary-container

@accent-pressed-color / @accent-pressed-color-dark

#d0bcff

#4f378b

state-pressed

Redeclare the accent variable inside the #Constants block of your app’s theme.css. The framework’s CSS compiler exports any --name declaration in #Constants as a @name theme constant, so the value flows through the same binding pass that handles runtime overrides:

#Constants {
    includeNativeBool: true;
    darkModeBool: true;

    /* Override the native theme's accent palette. The compiler picks
       up these declarations from #Constants and exports them as
       @accent-color / @accent-color-dark theme constants so every
       UIID bound to var(--accent-color) in the parent native theme
       picks them up at app launch - no per-UIID rule edit needed. */
    --accent-color: #ff2d95;
    --accent-color-dark: #ff2d95;
    --accent-pressed-color: #c71a75;
    --accent-pressed-color-dark: #c71a75;
    --accent-on-color: #ffffff;

    /* Material 3 RaisedButton uses a separate "container" tonal
       pair; iOS ignores these (no bindings reference them) so it's
       safe to set them unconditionally. */
    --accent-container-color: #ff2d95;
    --accent-container-color-dark: #ff2d95;
    --accent-on-container-color: #ffffff;
    --accent-on-container-color-dark: #ffffff;
}

Bindings that reference a constant you didn’t override stay at their baked-in default, so a partial override (for example, just --accent-color) is fine.

14.7.2. Runtime override

For dynamic theming - in-app accent toggles, branded flavours, A/B tests - push the same constants through UIManager.addThemeProps after the theme has been installed:

Hashtable override = new Hashtable();
override.put("@accent-color", "ff2d95");
override.put("@accent-color-dark", "ff2d95");
override.put("@accent-pressed-color", "c71a75");
override.put("@accent-pressed-color-dark", "c71a75");
override.put("@accent-on-color", "ffffff");
// every colour that has a -dark twin needs both, or dark mode keeps the
// theme's original value for the half you left out
override.put("@accent-on-color-dark", "ffffff");
override.put("@accent-disabled-color", "f9a8d0");
override.put("@accent-disabled-color-dark", "7a1547");
override.put("@accent-container-color", "ff2d95");
override.put("@accent-container-color-dark", "ff2d95");
override.put("@accent-on-container-color", "ffffff");
override.put("@accent-on-container-color-dark", "ffffff");
UIManager.getInstance().addThemeProps(override);
// null before the first form is shown, which is exactly when a branded
// flavour applies its overrides; forms built afterwards pick the new
// props up on their own
Form current = CN.getCurrentForm();
if (current != null) {
    current.refreshTheme();
}

Values can be passed with or without the leading # and in any case; the runtime accepts "ff2d95", "#FF2D95", and the 3-digit shorthand "#f0a" interchangeably.

PaletteOverrideThemeScreenshotTest in the hellocodenameone test suite exercises this path against both native themes, light + dark.

14.7.3. When the override path doesn’t apply

The binding mechanism handles every accent UIID the shipped themes expose. For other widgets - or when you want to override a color the native CSS hard-codes (for example, the iOS success green on Switch.selected) - either:

  • Layer a per-UIID redeclaration in your app’s theme.css (see Customizing in your own theme below). Right when you want to tweak a UIID the binding vocabulary doesn’t already cover.

  • Or pass the specific UIID/state key directly into UIManager.addThemeProps (Switch.sel#bgColor etc.). Runtime, bypasses the binding system. Right when you need a one-off color tweak you can’t anchor in CSS.

14.8. Platform-specific UIIDs

A handful of UIIDs exist only because one platform draws them differently from the other. They’re still safe to override; you just want to know which platform’s screen the override is going to land on.

iOS-only behavior:

  • Toolbar, TitleArea, Title paint over the status-bar area. iOS reserves room above for the notch / status bar; in your capture this reads as a colored strip at the top of the Form. In production the device fills it with system content (signal, battery, time).

  • MultiButton is styled as an iOS Settings row (multi-line text ramp + chevron). Compare against Material 3’s denser list-item pattern.

Android-only behavior:

  • Toolbar doesn’t paint over the system status bar; the native Android status bar handles that.

  • Tabs use Material 3 top tabs (flat, underline-by-color). iOS modern renders Tabs as a bottom-anchored pill group via the tabPlacementInt constant - that constant is intentionally only set in the iOS theme so behavior stays consistent on each platform.

14.9. Switching CheckBox / RadioButton glyphs

The default check / radio glyphs are Material icons drawn by DefaultLookAndFeel. The theme can swap them via four optional constants (Material codepoints):

ConstantDefaultNotes

@checkBoxCheckedIconInt

MATERIAL_CHECK_BOX (E834)

Override to for example, MATERIAL_CHECK_CIRCLE (E86C / 59500) for an iOS-style filled circle. The iOS modern theme does this.

@checkBoxUncheckedIconInt

MATERIAL_CHECK_BOX_OUTLINE_BLANK (E835)

Match the empty version of whatever you chose above.

@radioCheckedIconInt

MATERIAL_RADIO_BUTTON_CHECKED (E837)

Filled circle with dot.

@radioUncheckedIconInt

MATERIAL_RADIO_BUTTON_UNCHECKED (E836)

Empty circle.

Set these in your #Constants { …​ } block; the icons rebuild on the next style refresh.

14.10. Component-specific tuning constants

ConstantEffect

@switchTrackScaleX, @switchTrackScaleY

Stretches the Switch track. Larger X = longer pill, larger Y = thicker. iOS uses 2.5 / 1.5; Material uses 3.0 / 0.9.

@switchThumbScaleY

Scales the Switch thumb relative to the track. iOS 1.4, Material 1.5.

@switchThumbPaddingInt, @switchThumbInsetMM

Pixels of breathing room between thumb and track.

@switchTrackOffOutlineWidthMM, @switchTrackOffOutlineColor

iOS draws a faint ring around the off-state track; Material does not.

@tabPlacementInt

Component.TOP (0), Component.BOTTOM (2), Component.LEFT (1), Component.RIGHT (3). Where the Tabs widget anchors its tab bar. Set in iOS modern theme to 2 for the iOS 26 bottom-bar look; unset on Android (Tabs at top).

@tabsFillRowsBool, @tabsGridBool

Distribute tabs evenly across the bar.

@tabsAnimatedIndicatorBool

true paints a thin colored underline below the currently selected tab and tweens its x / width between tabs on selection change. Both modern themes set this to true. Off in the framework default; opt-in for legacy themes by setting it explicitly.

@tabsAnimatedIndicatorDurationInt

Duration of the indicator tween in milliseconds (default 200, matching Material 3’s NavigationBar spec).

@tabsAnimatedIndicatorThicknessMm

Indicator underline thickness in millimetres (default 1).

@pullToRefreshModernBool

true switches the pull-to-refresh visual from the legacy rotating-arrow + text Label stack to a thin circular arc spinner painted directly via Graphics.drawArc. The arc sweep grows from 0° to ~330° proportional to the user’s pull, then spins continuously once the refresh task fires. Both modern themes set this to true. Color comes from the TabIndicator UIID’s fg (consistent with the animated tab indicator), falling back to the Title fg if unset.

@pullToRefreshIndicatorDiameterMm

Outer diameter of the modern arc spinner in millimetres (default 8). Drives the gesture threshold too — the user must pull this distance plus a small margin to fire the refresh task.

@pullToRefreshIndicatorStrokeMm

Stroke thickness of the modern arc spinner in millimetres (default 0.6).

@darkModeBool

true enables $DarkUIID resolution when the app is in dark mode. Both modern themes set this; user themes that want dark mode also need to set it.

14.11. iOS 26 Liquid Glass tab selection morph

When the iOS modern theme renders Tabs as a bottom bar (tabPlacementInt: 2) the selected tab is marked by a Liquid Glass drop — a frosted, magnifying glass capsule painted over the (dark) icons rather than a flat highlight. On a selection change the drop springs across to the new tab, elongating while it travels and settling with a small overshoot. The selected blue is supplied by the drop itself (a luminance-keyed dark→accent tint of the glyph beneath it), so the color travels with the drop instead of snapping between tabs.

iOS 26 Liquid Glass tab selection morph

On iOS the drop is rendered live by a Metal fragment shader that samples the bar beneath it on the GPU, so the morph runs at frame rate with no screen read-back; other platforms and the simulator use the equivalent CPU lens. The effect is opt-in: the iOS modern theme sets tabsSelectionCapsuleBool: true and glassMaterialBool: true. Without the glass flag the capsule degrades to a plain translucent pill.

The morph is developed against the real OS control: the frame-by-frame comparison below shows the native iOS 26 tab bar (left) and the Codename One render (right) at matching moments of the selection travel.

Native iOS 26 tab bar vs the Codename One morph, frame by frame

The animation is controlled by a named motion preset plus a handful of high-level knobs. The detailed envelopes (stretch, squash, lift, lens magnification, aberration, tint timing) live inside the preset in the motion model (TabSelectionMorph), where they’re pinned by unit test and validated frame-by-frame by the fidelity suite — a theme picks a preset and scales it rather than tuning a dozen loose constants:

ConstantiOS modernEffect

@tabsSelectionCapsuleBool

true

Master switch for the selection drop. Off falls back to the legacy colored underline indicator (tabsAnimatedIndicatorBool).

@tabsMorphPreset

ios26

The named motion preset: ios26 (the measured Liquid Glass morph) or subtle (half the deformation and optics, for a calmer selection change).

@tabsAnimatedIndicatorDurationInt

480

Morph duration in milliseconds. Lower is snappier.

@tabsMorphLensIntensityPct

100

Scales the drop’s optics (magnification, chromatic aberration, accent tint) around the preset: 0 is an optically flat drop, 200 doubles the lens strength. Geometry is unaffected.

@tabsMorphSpringPct

100

Scales the settle overshoot: 0 stops dead on a plain ease-out, 200 doubles the bounce.

@tabSelLensTintColorInt

0x0a84ff

The accent the dark glyph is tinted to inside the drop.

The iOS 27 theme uses a third preset, ios27, which is time based rather than a fixed-duration morph. Its curves are measured from a real UITabBarController on the iOS 27 simulator, frame by frame, and played back by TabGlassMotion:

  • on a tap the selection lens lifts: it grows, its platter fades so it becomes clear glass, and the tab content seen through it magnifies;

  • it travels on a spring, stretching while it accelerates and squashing on arrival in proportion to the distance, then settles with a second, smaller wobble;

  • the whole bar — glass, content and lens — pulses in scale, and a soft glow spreads from where the finger touched.

The motion follows the gesture, not just the tap, the way UIKit’s does:

  • holding a press keeps the lens lifted and the bar grown until the finger lifts;

  • pressing the selected tab lifts and pulses the lens in place;

  • dragging along the bar pulls the lens with the finger (a scrub), and letting go selects the tab under the finger, with the lens settling there;

  • every release ends in a small wobble about a second after the finger lifts.

The accent color lives in a second copy of the tab content, visible only through the lens, so the color travels with the glass. Where the port supports Graphics.colorMatrixRegion (iOS, macOS, the JavaScript port and the simulator), the tab content is vibrant like UIKit’s: each glyph pixel is a color matrix of the glass behind it rather than flat paint, so labels and the accent pick up the colors under the bar. The selection platter is likewise the bar’s own glass seen through a color matrix, not a translucent fill. With this preset the tabs are painted with their normal style outside the lens and with their pressed style inside it: the theme’s Tab/TabIcon colors are the unselected ones and Tab.pressed/TabIcon.pressed the selected ones. Tab backgrounds should stay transparent.

The motion reaches past the bar’s visible pill, so the bar reserves room for it inside its own bounds: the theme widens the TabsContainer padding by the slack, narrows its margin by the same amount so the pill stays where it was, and names the slack in constants. Everything the motion paints then stays inside the bar’s own bounds and repaint region.

ConstantiOS 27Effect

@tabsMorphPreset

ios27

Selects the measured time-based motion described above.

@tabsGlassSlackXMm / @tabsGlassSlackYMm

2 / 1

How much of the TabsContainer padding is motion slack rather than pill.

@tabSelInsetMm

0.66

Inset of the resting lens inside the pill.

@tabSelPillColorInt / @tabSelPillAlphaInt

0 / 35

Color and alpha of the resting selection platter on ports without colorMatrixRegion.

@tabSelPillColorDarkInt / @tabSelPillAlphaDarkInt

0 / 67

The same platter in the dark appearance; defaults to the light values.

@tabGlassLensLightAlphaInt

28

White wash of the lifted lens, which reads brighter than the bar.

@tabGlassRimAlphaInt / @tabGlassEdgeAlphaInt

150 / 110

Alpha of the lifted lens’s bright inner rim and dark edge ring, scaled by the lift.

@tabGlassGlowPct

40

Strength of the touch glow; 0 turns it off.

@tabGlassSpecularAlphaInt / @tabGlassSpecularDarkAlphaInt

145 / 90

Peak alpha of the bright rim along the bar’s top and bottom edges; 0 (the default) draws none.

Similarly, the Liquid Glass backdrop materials themselves are typed, named recipes (GlassRecipe): a theme assigns chrome (edge-anchored bars), pill (the floating tab bar) or panel (buttons, dialogs, panels — the default) per UIID with <UIID>GlassRecipe constants, e.g. ToolbarGlassRecipe: "chrome". Each recipe bundles the measured saturation/scale/offset color transform and edge optics of one native material, so similar glass surfaces can’t drift apart one constant at a time.

The drop’s glass cues (rim shrink, refraction, glare, edge shadow) fade out as it settles, so a resting selection reads as a calm frosted pill rather than a distorting lens. Every motion preset is validated deterministically: the fidelity suite freezes the morph at the fixed progress points 0, 10, 25, 50, 75, 90 and 100 percent, regression-compares each frame, and pins the same points numerically against the motion model in unit tests. The Switch thumb uses the same discipline for its liquid droplet morph (switchLiquidGlassBool, stretch/squash while sliding, frame-validated):
iOS 26 liquid-glass switch droplet

14.12. Customizing in your own theme

Your app’s theme.css inherits from the installed native theme:

#Constants {
    includeNativeBool: true;
    darkModeBool: true;
}

/* Tweak only what's different. Everything you do not redeclare
   keeps coming from the native theme. */
RaisedButton { background-color: #d81b60; }
RaisedButton.pressed { background-color: #b71c5c; }
RaisedButton.disabled { background-color: #ffd6e2; color: #ffffff; }

@media (prefers-color-scheme: dark) {
    RaisedButton { background-color: #ff80ab; color: #4a0026; }
    RaisedButton.pressed { background-color: #f06292; }
}

The user’s CSS is layered on top of the native theme at app launch, so refresh / restart picks the override up.

For a wholesale accent rebrand prefer redeclaring the --accent-color (etc.) variables in your #Constants block - see Override from your app’s theme.css above. That single declaration fans out through every UIID bound to the variable in the native theme, no per-UIID rules required. Per-UIID redeclaration here remains the right choice for tweaking non-accent properties (typography, spacing, surface fills) the binding vocabulary doesn’t cover.

14.13. Inheriting from a native UIID

cn1-derive lets a custom UIID start from one of the native theme’s UIIDs and refine it:

DangerButton {
    cn1-derive: RaisedButton;
    background-color: #d32f2f;
}
DangerButton.pressed {
    cn1-derive: RaisedButton;
    background-color: #b71c1c;
}

cn1-derive works best when the relationship is child refines parent. Avoid deriving across unrelated UIIDs (for example, a TitleArea that derives from Toolbar) - inline the properties instead.

14.14. Translucency, glass effects, and the test harness

The iOS modern Dialog and Tabs use translucent surfaces (rgba with alpha < 1) so any backdrop reads through the widget. To exercise this in screenshot tests, the hellocodenameone fidelity tests opt-in to a diagonal-stripe textured backdrop on the form (useTexturedBackdrop() in DualAppearanceBaseTest). Translucent widgets show their see-through tint against the stripes; opaque widgets cover the stripes entirely.

CSS backdrop-filter: blur(<length>) and filter: blur(<length>) are now recognized by the compiler and persisted in the theme as the backdropFilterBlur and filterBlur Style properties (also reachable programmatically via Style#getBackdropFilterBlurRadius() / Style#getFilterBlurRadius()). Image-level blur is hardware-accelerated (CIGaussianBlur on iOS, RenderScript / RenderEffect on Android, JHLabs GaussianFilter in the simulator). Painting the blur into the component framebuffer is still being wired up; until that lands, the rgba approximation above remains useful for translucent surfaces.

15. CSS

In this chapter this guide covers theming with CSS in Codename One.

CSS Changes don’t Require a Recompile
You can change the CSS values while the simulator is running and the changes will reflect in the simulator within a few seconds

15.1. Activating CSS

Codename One applications always use the resource file. The CSS support compiles a file in CSS syntax to a Codename One resource file and adds it to the application. In runtime the CSS no longer exists and the file acts like a regular theme file.

New Maven projects enable CSS by default with codename1.cssTheme=true in common/codenameone_settings.properties. Add that property manually if you are migrating an older project where CSS support is disabled.

Once enabled your theme.res file will regenerate from a CSS file that resides under the css directory. Changes you make to the CSS file will instantly update the simulator as you save. But, there are some limits to this live update so sometimes a simulator restart would be necessary.

15.2. Layering one theme over another

Compiling CSS in a Maven project rewrites the build’s theme.res in place. The compiler reads src/main/css, merges in whatever CSS the project’s dependencies contribute, and writes the result as the theme the app loads. What it doesn’t do is keep an older Designer resource underneath: there’s no second file and no overlay involved.

Something does sit beneath the compiled styles, though. The theme.css a new project starts with sets includeNativeBool: true, and UIManager installs the platform’s native theme before it applies the compiled one, so a property you leave out of CSS can still arrive from there.

Layering is a separate mechanism that you ask for explicitly. A theme constant named OverlayThemes holds a comma-separated list of resource names, each without the .res extension.

Write that name bare wherever you author it. The Designer’s constant editor and a CSS #Constants block both add the @ that theme constants are stored with, exactly as they do for includeNativeBool or commandBehavior, so typing @OverlayThemes in either place produces @@OverlayThemes and nothing ever reads it. The stored key is @OverlayThemes, which is what you see in the resource and what code setting the property directly has to use.

In a CSS #Constants block, quote the value whenever you name more than one overlay. The compiler keeps only the first value it parses out of the declaration, so OverlayThemes: base,extra; loses extra without a word while OverlayThemes: "base,extra"; keeps both.

When UIManager finishes building a theme it reads that constant, opens each name in turn from the resource root and applies its properties over the ones already loaded, so a later overlay beats an earlier one and every overlay beats the theme that named them.

Reach for it when you want to override a theme you don’t own — one shipped inside a cn1lib, for instance — without editing the original. It’s also what the Designer’s CSS installer writes when it adds a stylesheet on top of an existing Designer theme, which is a migration path for older projects rather than the way a CSS project is built today.

An overlay is loaded by name from the resource root. A name the build doesn’t produce is skipped with a message on the error stream and the app carries on with the theme it already has, so a typo shows up as styling that never applies rather than as a build failure.

15.3. Supported CSS selectors

Since Codename One stylesheets are meant to be used with Codename One component hierarchies instead of XML/HTML documents, selectors work a little differently.

  1. All selectors (with some specific exceptions discussed below) are interpreted as UIIDs.

  2. Only 4 predefined CSS classes are supported:

    • .pressed: Targets the component when in "Pressed" state.

    • .selected: Targets the component when in "Selected" state.

    • .unselected: Targets the component when in "Unselected" state.

    • .disabled: Targets the component when in "Disabled" state.

      If no class is specified, then the selector targets "all" states of the given component.

The following are a few possible selectors you can include in your stylesheet.

  1. Button: Defines styles for the "Button" UIID.

  2. Button.pressed: Defines styles for the "Button" UIID’s "pressed" state.

  3. Button, TextField, Form: Defines styles for the Button, TextField, and "Form" UIIDs.

The following example creates a simple button with a border, and text aligned center. By default the button will have a transparent background, but when it’s pressed, it will have a gray background:

Button {
    text-align: center;
    border: 1pt solid gray;
    background-color: transparent;
}

Button.pressed {
    background-color: gray;
}

15.3.1. Inheriting properties using cn1-derive

The following example defines a custom Button style named "MyButton" that inherits all the styles of Button but changes the background color to blue:

MyButton {
   cn1-derive: Button;
   background-color: blue;
}

15.4. Special selectors

15.4.1. #device

The #Device selector allows you to define which device resolutions this CSS file should target. Mutli-images generated from this style-sheet will be included variants for device resolutions in the range (min-resolution, max-resolution) as defined in this section. By default all resolutions are generated:

#Device {
    min-resolution: 120dpi;
    max-resolution: 480dpi;
    resolution: 480dpi;
}

15.4.2. #constants

The #Constants selector allows you to specify theme constants.

For example:

#Constants {
    includeNativeBool: true;
    commandBehavior: "Side";
    defaultFontSizeInt: 18;
    defaultDesktopFontSizeInt: 14;
    defaultSourceDPIInt: "0";
    menuImage: "menu.png";
}

MenuIcon {
    background-image: url(images/menu.png);
}

In the above example, the constants referring to an image name as a string requires that the image exists in one of the following locations:

  • res/<cssfilename>/<imageName>

  • ../res/<cssfilename>/<imageName>

  • ../../res/<cssfilename>/<imageName>

It must also have been defined as a background image in some selector in this CSS file.

15.4.3. Default

The Default selector is special in that it will set properties on the theme’s "default" element. The default element is a special UIID in Codename One from which all other UIIDs in the same theme are derived. This is a good place to set things like default fonts or background-colors.

15.5. Standard CSS properties

  • padding (and variants)

  • margin (and variants)

  • border (and variants)

  • border-radius

  • background (Usage below)

  • background-color

  • background-repeat

  • background-image

  • border-image

  • border-image-slice

  • font (Usage is covered in the following font section)

  • font-family (Usage is covered in the following font section)

  • font-style (Usage is covered in the following font section)

  • font-size (Usage is covered in the following font section)

  • @font-face (Usage is covered in the following font section)

  • color

  • text-align

  • text-decoration(Usage below)

  • opacity

  • box-shadow

  • width ( used for generating background-images and borders)

  • height ( used for generating background-images and borders)

15.6. Custom properties

cn1-source-dpi

Used to specify source DPI for multi-image generation of background images. Accepted values: 0 (Not multi-image), 120 (Low res), 160 (Medium Res), 320 (Very High Res), 480 (HD), Higher than 480 (2HD). If not specified, the default value will be the value of the defaultSourceDPIInt theme constant, if specified, or 480, if not specified.

cn1-background-type

Used to explicitly specify the background-type that should be used for the class.

cn1-9patch

Used to explicitly specify the slices used when generating 9-piece borders. Deprecated - Use border-image and border-image-slice for 9-piece borders.

cn1-derive

Used to specify that this UIID should derive from an existing UIID.

15.7. CSS variables

As of CodenameOne 7.0, you can use variables in your CSS file via the var() CSS function. For example:

#Constants {
    --header-color: blue;
}

Header {
    color: var(--header-color, blue);
}

The var() function can be used inside property values. That is, You can’t use it in property names or selectors.

The var() grammar follows the browser CSS form:

#Constants {
    --custom-property-name: #0066cc;
}

ExampleSelector {
    color: var(--custom-property-name, #0066cc);
}

The <custom-property-name> must begin with two dashes (--).

The <declaration-value> is the fallback value that will be used if the variable hasn’t been defined in the CSS file. The fallback value may include commas.

Examples

Listing 11. Example defining and using a CSS variable
#Constants {
    --main-bg-color: red;
}

MyContainer {
    background-color: var(--main-bg-color);
}
Listing 12. Example using a fallback value
#Constants {
    --main-bg-color: red;
}

MyContainer {
    background-color: var(--main-bg-color, blue);
}

See the MDN docs for more details about the CSS variable spec.

15.8. CSS properties

This section isn’t as comprehensive as it should be due to the breadth of CSS.

15.8.1. Text-decoration

underline

Underlines text. For example, text-decoration: underline;

overline

Overlines text. For example, text-decoration: overline;

line-through

Strikes through text. For example, text-decoration: line-through;

none

No text decoration. For example, text-decoration: none;

cn1-3d

3D text. For example, text-decoraton: cn1-3d; cn1-3d screenshot

cn1-3d-lowered

3D lowered text. For example, text-decoration: cn1-3d-lowered; cn1-3d-lowered screenshot

cn1-3d-shadow-north

3D text with north shadow. For example, text-decoration: cn1-3d-shadow-north; cn1-3d-shadow-north screenshot

For other CSS font settings see the Fonts section

15.8.2. Border

This library supports the border property and most of its variants (for example, border-width, border-style, and border-color. It will try to use native Codename One styles for generating borders if possible. If the border definition is too complex, it will fall-back to generating a 9-piece image border at compile-time. This has the effect of making the resulting resource file larger, but will produce good runtime performance, and a look that’s faithful to the provided CSS.

The algorithm used to determine whether to use a native border or to generate a 9-piece image, is complex, but the following guidelines may help you if you wish to design borders that can be rendered natively in CN1:

  • Non-pixel units border-width. (Except with the cn1-round-border and cn1-pill-border styles)

  • Using the border-radius directive.

  • Using box-shadow (Unless using cn1-round-border or cn1-pill-border styles)

  • Using a background gradient in combination with a border or any kind

  • Using a different border-width, border-style, or border-color for different sides of the border

  • Using a filter

You can open the resulting theme file in the designer and inspect it to see if an image was generated

Generating the image triggers slower CSS compilation and a larger binary, so tune the CSS so it avoids this fallback.

Round borders

Rounded borders can be achieved in a few different ways. The easiest methods are:

  • The cn1-round-border style. This will render a circular round border in the background natively. That is, this doesn’t require generation of an image border

  • The cn1-pill-border style. This will render a pill-shaped border in the background natively. This also doesn’t require generation of an image border

  • The border-radius property. This will round the corners of the border. If the style can be achieved using the RoundRectBorder in CodenameOne, then it will use that border. If not, this will cause the style to be generated as an image border

A border generated from border-radius follows the CSS box model, so the radius rounds the component without changing its size. A radius larger than the component scales down to fit it. This differs from a RoundRectBorder you create in Java, which grows the component to twice the radius so the corners are always drawn in full. Use cn1-pill-border when a shape has to stay a pill at any height.

Examples using cn1-round-border

RoundBorder {
    border: 1px #3399ff cn1-round-border;
    text-align: center;
    margin: 2mm;
    padding: 3mm;
}

RoundBorderFilled {
    background: cn1-round-border;
    background-color: #cccccc;
    text-align: center;
    margin: 2mm;
    padding: 3mm;
}

Examples using cn1-pill-border

PillBorder {
    border: 1pt #3399ff cn1-pill-border;
    text-align: center;
}

PillBorderFilled {
    background: cn1-pill-border;
    background-color: #3399ff;
    color: white;
    text-align: center;
}

Examples using border-radius

RoundRectLabel {
    background-color: red;
    border-radius: 2mm;
}

cn1-pill-border and cn1-round-border don’t support the standard CSS box-shadow property. This is because the box-shadow property parameters don’t map onto the shadow parameters for the Codename One RoundBorder class. To get shadows on the cn1-pill-border, you should use one or more of the following CSS properties:

  • cn1-box-shadow-spread: Accepts values in any scalar unit (for example, px, mm, cm, etc.). This maps directly to the border’s shadowSpread property.

  • cn1-box-shadow-h: Accepts values in real values or integers (not a scalar unit). This maps directly to the border’s shadowX property.

  • cn1-box-shadow-v: Accepts values in real values or integers (not a scalar unit). This maps directly to the border’s shadowY property.

  • cn1-box-shadow-blur: Scalar value. Maps to the border’s shadowBlur property.

  • cn1-box-shadow-color: The shadow color

  • cn1-box-shadow-inset: Set to inset to render an inner shadow instead of the default outer shadow spread.

Using the regular CSS box-shadow in conjunction with border-radius will cause a 9-piece border to be generated rather than mapping to the RoundRectBorder. If, but, you use the cn1-box-* properties for the shadow instead, it will use the RoundRectBorder—assuming that no other styles are specified that trigger an image border to be generated.

Codename One also exposes per-corner elliptical radius controls that map to the RoundBorder’s X/Y radii. You can set them directly with `cn1-border-top-left-radius-x / cn1-border-top-left-radius-y (and the equivalent top-right, bottom-left, and bottom-right pairs) to fine tune horizontal and vertical curvature independently. The CSS parser automatically populates these properties when you use standard border-radius syntax, including the longhand declarations and the border-radius: <x-radii> / <y-radii> shorthand:

EllipticalBorder {
    border-radius: 2mm 4mm 6mm 1mm / 1mm 3mm 5mm 7mm;
    cn1-box-shadow-spread: 1.5mm;
    cn1-box-shadow-inset: inset;
}

In the example above, the four horizontal radii (2mm 4mm 6mm 1mm) populate the cn1-border--radius-x properties clockwise from the top-left corner. The four values after the slash fill the matching cn1-border--radius-y entries. Setting cn1-box-shadow-inset: inset; converts the shadow into an inset glow that follows the same elliptical curvature.

15.8.3. Background

The background property supports most standard CSS values for setting the background color, or background image.

9-piece Image borders always take precedence over background settings in Codename One. If your background directive seems to have no effect, it’s likely because the theme has specified a 9-piece image border for the UIID. You can disable the image border using a directive like border: none
Background images

See Images

Gradients

The full CSS gradient range is natively supported: linear-gradient, radial-gradient, conic-gradient, repeating-linear-gradient, and repeating-radial-gradient — all with arbitrary angles, unlimited multi-stop colors (with optional position hints), and full radial shape/extent control. The compiled theme resource file carries a compact descriptor and the gradient is rendered at runtime by the platform-native graphics API (Java2D on JavaSE, LinearGradient/RadialGradient/SweepGradient on Android, Core Graphics / Core Image on iOS).

CSS gradient examples
Figure 205. CSS gradient functions rendered side-by-side from the framework’s screenshot test (top to bottom: linear at an arbitrary angle, linear to <side>, linear with mismatched alphas, radial farthest-corner, radial ellipse, conic, repeating-linear, repeating-radial).

linear-gradient

Any angle in degrees, radians, or turn, or the canonical to <side> / to <side1> <side2> directions. Two or more stops, each with optional position percentage; positions left blank between fixed anchors are autodistributed:

LinearGradientA { background: linear-gradient(0deg, #cccccc, #666666); }
LinearGradientB { background: linear-gradient(to top, #cccccc, #666666); }
LinearGradientC { background: linear-gradient(45deg, #eaeaea, #666666); }
LinearGradientD { background: linear-gradient(135deg, #ff0080 0%, #ff8c00 50%, #40e0d0 100%); }
LinearGradientE { background: linear-gradient(to bottom right, #ff0066, #000033); }
LinearGradientF { background: linear-gradient(90deg, rgba(255, 0, 0, 0.6), blue); }

radial-gradient

Full CSS radial syntax: circle or ellipse, any of the four extent keywords (closest-side / closest-corner / farthest-side / farthest-corner, defaulting to farthest-corner), explicit radii as percentages, an optional at <position> clause with side keywords or percentages, and two or more multi-stop colors:

RadialGradientA { background: radial-gradient(circle, gray, white); }
RadialGradientB { background: radial-gradient(ellipse closest-side at 25% 75%, #ffffff, #000000); }
RadialGradientC { background: radial-gradient(circle farthest-corner at right, #ffffee, #006666 60%, #000011 100%); }

conic-gradient

Sweep / pie-style gradient. Optional from <angle> (CSS convention: 0° points up, sweep is clockwise) and at <position> prefix, followed by the stop list:

ConicGradientA { background: conic-gradient(red, yellow, green, blue, red); }
ConicGradientB { background: conic-gradient(from 45deg at 50% 50%, #ff0066 0%, #ffcc00 25%, #00cc66 50%, #0066ff 75%, #ff0066 100%); }

repeating-linear-gradient / repeating-radial-gradient

Identical syntax to their non-repeating counterparts. The stop pattern tiles outward to fill the bounding box, ideal for stripes and rings:

MyStripe { background: repeating-linear-gradient(45deg, #eeeeee 0%, #eeeeee 10%, #cccccc 10%, #cccccc 20%); }
MyTarget { background: repeating-radial-gradient(circle at center, #ffffff 0%, #ffffff 20%, #cc3333 20%, #cc3333 40%); }
filter and backdrop-filter

CSS filter and backdrop-filter accept a chain of functions. Two storage forms land on the corresponding Style:

  • blur(<length>) → filterBlurRadius / backdropFilterBlurRadius (a single pixel value).

  • brightness, contrast, grayscale, hue-rotate, invert, opacity, saturate, sepia → filterColorMatrix / backdropFilterColorMatrix (a 4×5 color matrix; a multi-function chain composes into a single matrix in CSS order):

MyOverlay {
    background: rgba(0, 0, 0, 0.4);
    backdrop-filter: blur(12px);
}

MyBlurredImage {
    filter: blur(4px);
}

MyFaded {
    filter: brightness(0.7) contrast(1.15);
}

MyGrayscale {
    filter: grayscale(1);
}

MySepia {
    filter: sepia(0.8) saturate(1.1);
}
CSS filter blur visual
Figure 206. filter: blur() applied at a graphics primitive level — the framework’s screenshot test renders RGB stripes and a gradient, then blurs both via Graphics.gaussianBlur(…​). Component-level paint-time integration of filter: declarations is in progress; until it lands, set the radius / matrix on a Style and consume them via the corresponding Graphics primitives manually.

filter applies to the component’s own painted content; backdrop-filter applies to whatever is painted behind. The radii / matrices are also exposed on Style (getFilterBlurRadius(), getFilterColorMatrix(), getBackdropFilterBlurRadius(), getBackdropFilterColorMatrix()) so they can be set programmatically. Hardware blur is used where available (Core Image on iOS, RenderScript/RenderEffect on Android, JHLabs GaussianFilter on JavaSE simulator); ports without a fast path fall back to a software Gaussian.

15.8.4. cn1-background-type

It also supports some special Codename One values, which are identifiers with a "cn1-" prefix. The following special values are available. They map to the standard Codename One values you discussed in the theming chapter:

  • cn1-image-scaled

  • cn1-image-scaled-fill

  • cn1-image-scaled-fit

  • cn1-image-tile-both

  • cn1-image-tile-valign-left

  • cn1-image-tile-valign-center

  • cn1-image-tile-valign-right

  • cn1-image-tile-halign-top

  • cn1-image-tile-halign-center

  • cn1-image-tile-halign-bottom

  • cn1-image-align-bottom

  • cn1-image-align-left

  • cn1-image-align-right

  • cn1-image-align-center

  • cn1-image-align-top-left

  • cn1-image-align-top-right

  • cn1-image-align-bottom-left

  • cn1-image-align-bottom-right

  • cn1-image-border

  • cn1-none

  • cn1-round-border

  • cn1-pill-border

15.9. Images

Images are supported as both "inputs" of the stylesheet, and as outputs to the compiled resource file. "Input" images are specified via the background-image property in a selector. "Output" images are always saved as multi-images inside the resource file.

15.9.1. Image DPI and device densities

To appropriately size the image, the CSS compiler needs to know what the source density of the image is. For example, if an image is 160×160 pixels with a source density of 160dpi (that is, medium density - or the same as an iPhone 3G), then the resulting multi-image will be sized at 160×160 for medium density devices and 320×320 on high density devices (for example, iPhone 4S Retina) - which will result in the same perceived size to the user of 1×1 inch.

However, if the image has a source density of 320dpi, then the resulting multi-image would be 80×80 pixels on medium density devices and 160×160 pixels on high density devices.

Some images have this density information embedded in the image itself so that the CSS processor will know how to resize the image. But, it’s better to explicitly document your intentions by including the cn1-source-dpi property as follows:

SomeStyle {
    background-image: url(images/my-image.png);
    cn1-source-dpi: 160;
}
cn1-source-dpi values are meant to fall into threshold ranges. Values less than or equal to 120, are interpreted as low density. 121 - 160 are medium density (iPhone 3GS). 161 - 320, high density (iPhone 4S). 321 - 480 == HD. 481 and higher == 2HD. In general, you should try to use images that are one of these DPIs exactly: 160, 320, or 480, then images will be scaled up or down to the other densities accordingly.

15.9.2. Multi-Images vs regular images

By default all images are imported as multi-images (unless you define the defaultSourceDPIInt theme constant). If you want to import an image as a "regular" image, you can set cn1-source-dpi to 0. For example:

SomeStyle {
    background-image: url(images/my-image.png);
    cn1-source-dpi: 0;
}

You can change the default source DPI for the whole stylesheet by adding defaultSourceDPIInt: 0 to the theme constants. For example:

#Constants {
  defaultSourceDPIInt: 0;
}

Most application templates in Codename One Initializr include this constant by default.

15.9.3. Multi-Images as inputs

If you’ve already generated images in all the appropriate sizes for all densities, you can provide them in the same file structure used by the Codename One XML resource files: The image path is a directory that contains images named after the density that they’re intended to be used for. The possible names include:

  • verylow.png

  • low.png

  • medium.png

  • high.png

  • veryhigh.png

  • 560.png

  • hd.png

  • 2hd.png

  • 4k.png

For example, Given the CSS directives:

MyStyle {
    background-image: url(images/mymultiimage.png);
}

The files would look like:

css/
 +--- mycssfile.css
 +--- images/
       +--- mymultiimage.png/
             +--- verylow.png
             +--- low.png
             +--- medium.png
              ... etc.
Multi-image inputs are supported for local URLs. You can’t use remote (for example, http://) urls with multi-image inputs

15.9.4. Image constants

Theme constants can be images. The convention is to suffix the constant name with "Image" so that it will be treated as an image. Also to the standard url() notation for specifying a constant image, you can provide a simple string name of the image, and the CSS processor will try to find an image by that name specified as a background image for one of the styles. If it can’t find one, it will look inside a special directory named "res" (located in the same directory as the CSS stylesheet), inside which it will look for a directory named the same as the stylesheet, inside which it will look for a directory with the specified multi-image. This directory structure is the same as used for Codename One’s XML resources directory.

For example, In the CSS file "mycssfile.css":

radioSelectedFocusImage: "Radio_btn_Press.png";

Will look for a directory located at res/mycssfile.css/Radio_btn_Press.png/ with the following images:

  • verylow.png

  • low.png

  • medium.png

  • high.png

  • veryhigh.png

  • 560.png

  • hd.png

  • 2hd.png

  • 4k.png

It will then create a multi-image from these images and include them in the resource file.

15.10. Image recipes

15.10.1. Import multiple images in single selector

It’s quite useful to be able to embed images inside the resource file that’s generated from the CSS stylesheet so that you can access the images using the Resources.getImage() method in your app and set it as an icon on a button or label. In this case, it’s easier to create a dummy style that you don’t intend to use and include multiple images in the background-image property like so:

Images {
    background-image: url(images/NowLogo.png),
        url(images/Username-icon.png),
        url(images/Password-icon.png),
        url(images/Name-icon.png),
        url(images/Email-icon.png),
        url(images/SeaIce.png),
        url(images/Back-icon.png),
        url(images/Source-icon.png),
        url(images/Date-icon.png),
        url(images/Arrow-right.png),
        url(images/Share-icon.png),
        url(images/Text-icon.png),
        url(images/Comments-icon.png),
        url(images/RedPlanet.png),
        url(images/News-icon.png),
        url(images/Channels-icon.png),
        url(images/Bookmarks-icon.png),
        url(images/Overview-icon.png),
        url(images/Calendar-icon.png),
        url(images/Timeline-icon.png),
        url(images/Profile-icon.png),
        url(images/Widgets-icon.png),
        url(images/Settings-icon.png),
        url(images/Bookmark-icon.png);
}

Then in Java, you might do something like:

Resources theme = Resources.openLayered("/theme");

Label bookmark = new Label(theme.getImage("Bookmark-icon.png"));

15.10.2. Loading images from URLs

You can also load images from remote URLs. The compiled guide fixture uses a local asset so the build is deterministic and doesn’t depend on network access; application CSS can use the same url(…​) syntax with an HTTPS URL:

ImagesRemote {
    background-image: url(images/remote-logo.png);
}

15.10.3. Generating 9-Piece Image borders

css nine piece border

9-Piece image borders can be created using the image-border and image-border-slice properties.

For example:

NinePiece {
    border-image: url(images/dashbg_landscape.png);
}

In the above example you omitted the border-image-slice property, so it defaults to "40%," which means that the image is sliced 40% from the top, 40% from the bottom, 40% from the left, and 40% from the right.

If you want more specific "slice" points, you can add the border-image-slice property. For example:

NinePiece {
    border-image: url(images/dashbg_landscape.png);
    border-image-slice: 10% 49%;
}

NinePiece2 {
    border-image: url(images/dashbg_landscape.png);
    border-image-slice: 10% 49% 20%;
}

NinePiece3 {
    border-image: url(images/dashbg_landscape.png);
    border-image-slice: 10% 30% 40% 20%;
}

NinePiece4 {
    border-image: url(images/dashbg_landscape.png);
    border-image-slice: 10%;
}

15.10.4. Image backgrounds

Component backgrounds in Codename One are a common source of confusion for newcomers because there are 3 different properties that can be used to define what a component’s background looks like, and they have priorities:

  1. Background Color - You can specify an RGB color to be used as the background for a component.

  2. Background Image - You can specify an image to be used as the background for a component. Codename One includes settings to define how the image is treated, for example, scale/fill, tile, etc. If a background image is specified, it will override the background color setting - unless the image has transparent regions.

  3. Image Border - You can define a 9-piece image border which will effectively cover the entire background of the component. If an image border is specified, it will override the background image of the component.

A common scenario you may run into is trying to set the background color of a component and seeing no change when you preview the form, because the style had an image background defined - which overrides the background color change.

The potential for confusion is mitigated somewhat, but still exists when using CSS. You can make your intentions explicit by adding the cn1-background-type property to your style. Possible values include:

  • cn1-image-scaled

  • cn1-image-scaled-fill

  • cn1-image-scaled-fit

  • cn1-image-tile-both

  • cn1-image-tile-valign-left

  • cn1-image-tile-valign-center

  • cn1-image-tile-valign-right

  • cn1-image-tile-halign-top

  • cn1-image-tile-halign-center

  • cn1-image-tile-halign-bottom

  • cn1-image-align-bottom

  • cn1-image-align-left

  • cn1-image-align-right

  • cn1-image-align-center

  • cn1-image-align-top-left

  • cn1-image-align-top-right

  • cn1-image-align-bottom-left

  • cn1-image-align-bottom-right

  • cn1-image-border

  • cn1-none

  • none

Example setting background Image to scale fill
MyContainer {
    background-image: url(images/myimage.png);
    cn1-background-type: cn1-image-scaled-fill;
}

15.11. Image compression

CN1 resource files support both PNG and JPEG images, but PNG is the default. Multi-images that are generated by the CSS compiler will be PNG if they include alpha transparency, and JPEG otherwise. This is to try to reduce the file size as much as possible while not sacrificing quality.

15.12. Fonts

This library supports the font, font-size, font-family, font-style, font-weight, and text-decoration properties, as well as the @font-face CSS "at" rule for including TrueType and OpenType fonts.

15.12.1. font-family

By default, CN1’s native fonts are used. The appropriate native font is selected for the provided font-weight and font-style properties. You can also explicitly specify the native font you wish to use in the font-family property. For example:

SideCommand {
    font-family:  "native:MainThin";
}

If you omit the font-family directive altogether, it will use native:MainRegular. The following native fonts are available:

  1. native:MainThin

  2. native:MainLight

  3. native:MainRegular

  4. native:MainBold

  5. native:MainBlack

  6. native:ItalicThin

  7. native:ItalicLight

  8. native:ItalicRegular

  9. native:ItalicBold

  10. native:ItalicBlack

Using bundled fonts

If you want to use a font other than the built-in fonts, you’ll need to define the font using the @font-face rule. For example:

@font-face {
    font-family: "GuideDemoFont";
    src: url(res/GuideDemoFont-Regular.ttf);
}

Both TrueType (.ttf) and OpenType (.otf) files work, upper-case or lower-case. Every platform loads both formats through its own font API, so an OpenType font needs no conversion before you bundle it. Web-only formats such as .woff aren’t supported, and the CSS compiler fails the build on one rather than letting it reach the device.

The compiler reads each font at build time, so a file it can’t parse, or one with no PostScript name, fails the build. That second case is worth knowing about: iOS resolves fonts by their PostScript name, so a font without one renders everywhere except on an iOS device.

Then you’ll be able to reference the font using the specified font-family in any CSS element. For example:

MyLabel {
    font-family: "GuideDemoFont";
}
Where to put the font file

A relative src URL is resolved against the directory that holds the CSS file. You can keep font files directly beside theme.css, or in any subdirectory of it, whichever you prefer:

@font-face {
    font-family: "GuideRootFont";
    src: url(GuideRootFont.ttf);
}

@font-face {
    font-family: "GuideDemoFont Bold";
    src: url(res/GuideDemoFont-Bold.ttf);
}

The font does have to live somewhere under that directory, because only that directory is copied into the build. A font reached through ../, or one that doesn’t exist, fails the build rather than producing an app whose text falls back to the system font. Two @font-face rules naming different files that share a file name also fail, since fonts are deployed by file name alone and one would overwrite the other. Pointing two families at the same file is fine.

Family names, weights and styles

A font-family name that contains spaces must be quoted, both in the @font-face rule and wherever you reference it. An unquoted name is parsed as a list of separate identifiers, so font-family: GuideDemoFont Bold registers the family as GuideDemoFont and collides with your regular weight.

font-weight and font-style select between the built-in native: fonts, but they have no effect once font-family resolves to a @font-face rule. Declare one @font-face per weight and style you need, each with its own family name, as in the example above, then reference the right family from each UIID.

To change the base font of an entire theme, set font-family on the special Default selector, then override the UIIDs that need a bold or italic face:

Default {
    font-family: "GuideRootFont";
}

Title {
    font-family: "GuideDemoFont Bold";
}
Remote and GitHub-hosted fonts

The @font-face directive’s src property will accept both local and remote URLs. The guide fixture below uses a local font so the demo build remains offline and repeatable; application CSS can replace the URL with an HTTPS font URL:

@font-face {
    font-family: "GuideDownloadedFont";
    src: url(res/GuideDemoFont-Regular.ttf);
}

In this case, it will download the myfont.ttf file into the build directory alongside the merged CSS file, and reuse that copy on later builds so that it doesn’t have to make a network request every time. A mvn clean discards the cache and the next build downloads the font again.

Fonts are automatically copied next to the compiled theme.res when the CSS file is compiled, so that they’re distributed with the app and available at runtime. The copy uses the font’s file name only, which means two @font-face rules that point at identically named files in different directories will collide. The copy is also skipped when a file of that name is already there, so run mvn clean after you replace a font file with a different one of the same name.

GitHub URLs

Fonts hosted on GitHub are accessible using a special github: protocol to make it easier to reference such fonts. The compiled guide fixture uses a bundled icon font for offline validation; application CSS can use the same rule shape with a github: URL.

@font-face {
    font-family: "GuideIconFont";
    src: url(res/GuideIconFont.ttf);
}
If a third-party font repository moves or changes licensing, vendor the font into your project and reference it locally so builds stay reproducible.

15.12.2. font-size

It’s best practice to size your fonts using millimetres (rem) (or another "real-world" measurement unit such as inches (in), centimetres (cm), millimetres (mm). This will allow the font to be sized appropriate for all display densities. If you specify size in pixels (px), it will treat it the same as if you sized it in points (pt), where 1pt == 1/72 inches (one seventy-second of an inch).

If you size your font in percentage units (for example, 150%) it will set the font size relative to the medium font size of the platform. This is different than the standard behaviour of a web browser, which would size it relative to the parent element’s font size.

font-size: 150% is the same as font-size: 1.5rem.

You can use system fonts, true type fonts, and native fonts in your CSS stylesheet. True Type fonts need to be defined in a @font-face directive before they can be referenced. True-type fonts and native fonts have the advantage that you can specify their sizes in generic terms (for example, small, medium, large) and in more specific units such as millimeters (mm) or pixels (px).

Normalizing Default Font Size

When trying to make a design look "good" across multiple platforms it can be difficult to deal with the differing default font sizes on different platforms. You may spend hours tweaking your UI to look perfect on iPhone X, only to find out that the fonts are too small when viewed on an android device. You’ve now added theme constants to explicitly set the default font size in "screen-independent-pixels."

Note In this case, 1 screen-independent-pixel is defined as 1/160th of an inch on a device, and 1/96th of an inch on desktop. These values correspond to Android’s definition on device, and Windows' definition on the desktop.

If you add the following to your stylesheet, it will set the default font size to 18 screen-independent pixels (or 18/160th of an inch), which corresponds to the Android native default "medium" font size:

#Constants {
 defaultFontSizeInt: 18;
}
A value of 18 here gives best results across devices.

On the desktop, you may find that 18 is too big. You can define a default font size for tablet and desktop using defaultDesktopFontSizeInt and defaultTabletFontSizeInt respectively. A defaultDesktopFontSizeInt value gives results that match the macOS default font size:

#Constants {
 defaultFontSizeInt: 18;
 defaultDesktopFontSizeInt: 14;
}
How density is determined per platform

The conversion from "screen-independent pixels" or mm to physical pixels uses the device’s PPI. The two mobile ports compute it differently, which is worth keeping in mind when chasing per-platform sizing differences:

  • Android: uses density × 160 from DisplayMetrics, where density is the bucketed scale factor reported by Android (typically 1.0, 1.5, 2.0, 3.0, 3.5, 4.0). Because the buckets are coarse, a phone whose physical density falls between two buckets will report the lower one, so mm-based measurements may render slightly smaller than their nominal physical size.

  • iOS: uses an explicit per-device PPI lookup keyed on the screen’s pixel dimensions (for example, 460ppi for iPhone 12 / 13 / 14 / 15 / 16, 458ppi for the Plus and Pro Max models, 326ppi for older 2× devices). This is exact for catalogued devices. Any iPhone whose resolution isn’t in the table falls through to a default fallback (now 460ppi, since Apple has held that density steady for every non-Plus iPhone since the iPhone 12).

Net result: the same defaultFontSizeInt: 18 may render at slightly different absolute physical sizes on an iPhone vs. An Android device of similar physical size—typically the iOS rendering is the larger of the two when both are at the platform’s default text size. If you need cross-platform pixel-perfect sizing, use platform overrides (@iOS-platform-font-size, etc., via media queries) rather than relying on the conversion math alone.

When a new iPhone model ships with a resolution that isn’t yet in the iOS PPI table, mm-based measurements will use the fallback 460ppi, which is correct to within 1-2% for every modern iPhone. If you discover a model that should use a different value (for example a future Plus/Max device that returns to 458ppi), file an issue so the table can be updated.

15.12.3. text-decoration

15.12.4. Some sample CSS directives

@font-face {
    font-family: "GuideDemoFont";
    src: url(res/GuideDemoFont-Regular.ttf);
}

@font-face {
    font-family: "GuideDemoFont-Bold";
    src: url(res/GuideDemoFont-Bold.ttf);
}

PlainText0p5mm { font-size: 0.5mm; }
PlainText1mm { font-size: 1mm; }
PlainText2mm { font-size: 2mm; }
PlainText5mm { font-size: 5mm; }
PlainText10mm { font-size: 10mm; }
PlainText50mm { font-size: 50mm; }
PlainTextSmall { font-size: small; }
PlainTextMedium { font-size: medium; }
PlainTextLarge { font-size: large; }
PlainText3pt { font-size: 3pt; }
PlainText6pt { font-size: 6pt; }
PlainText12pt { font-size: 12pt; }
PlainText20pt { font-size: 20pt; }
PlainText36pt { font-size: 36pt; }

BoldText { font-weight: bold; }
BoldText1mm { font-weight: bold; font-size: 1mm; }
BoldText2mm { font-weight: bold; font-size: 2mm; }
BoldText3mm { font-weight: bold; font-size: 3mm; }
BoldText5mm { font-weight: bold; font-size: 5mm; }

ItalicText { font-style: italic; }
ItalicText3mm { font-style: italic; font-size: 3mm; }
ItalicBoldText { font-style: italic; font-weight: bold; }

PlainTextUnderline { text-decoration: underline; }
BoldTextUnderline { text-decoration: underline; font-weight: bold; }
ItalicTextUnderline { text-decoration: underline; font-style: italic; }

PlainText3d {
    text-decoration: cn1-3d;
    color: white;
    background-color: #3399ff;
}

BoldText3d {
    text-decoration: cn1-3d;
    font-weight: bold;
    color: white;
    background-color: #3399ff;
}

ItalicText3d {
    text-decoration: cn1-3d;
    font-style: italic;
    color: white;
    background-color: #3399ff;
}

PlainText3dLowered {
    text-decoration: cn1-3d-lowered;
    color: black;
    background-color: #3399ff;
}

BoldText3dLowered {
    text-decoration: cn1-3d-lowered;
    font-weight: bold;
    color: black;
    background-color: #3399ff;
}

ItalicText3dLowered {
    text-decoration: cn1-3d-lowered;
    font-style: italic;
    color: black;
    background-color: #3399ff;
}

PlainText3dShadow {
    text-decoration: cn1-3d-shadow-north;
    color: white;
    background-color: #3399ff;
}

BoldText3dShadow {
    text-decoration: cn1-3d-shadow-north;
    font-weight: bold;
    color: white;
    background-color: #3399ff;
}

ItalicText3dShadow {
    text-decoration: cn1-3d-shadow-north;
    font-style: italic;
    color: white;
    background-color: #3399ff;
}

MainThin {
    font-size: 200%;
    background: radial-gradient(circle at top left, yellow, blue 100%);
}

MainRegular0001 {
    font-family: "native:MainRegular";
    color: blue;
    border: 1px cn1-pill-border blue;
    padding: 2mm;
}

MainRegular0001.pressed {
    font-family: "native:MainRegular";
    background: cn1-pill-border blue;
    color: white;
    border: 1px solid white;
    padding: 2mm;
}

Heading {
    font-size: 4mm;
    font-family: "GuideDemoFont-Bold";
    color: black;
    padding: 2mm;
    text-align: center;
}

15.13. Media queries

You can use media queries to target styles to specific platforms, devices, and device densities. The following media queries are supported:

  1. platform-xxx - Target a specific platform. For example, platform-and, platform-ios, platform-mac, platform-win.

  2. density-xxx - Target a specific device density. For example, density—​low, density-low, density-medium, density-high, density—​high, density-hd, density-2hd, and density-560.

  3. device-xxx - Target a specific device type. For example, device-desktop, device-tablet, device-phone, device-tv (Apple TV / Android TV), and device-watch (Apple Watch / Wear OS). The device-tv and device-watch variants are selected at runtime when Display.isTV() / Display.isWatch() returns true, letting you adapt styling for the 10-foot TV UI or the small wearable screen.

Also to the Codename One specific media tokens above, the CSS compilers also recognize standard dark-mode media queries using prefers-color-scheme: dark. Rules inside these blocks are compiled into $Dark UIIDs automatically (for example, Button becomes $DarkButton, Button.selected becomes $DarkButton.selected).

Listing 13. Example: Dark-mode overrides with standard CSS media query syntax
Button {
    color: #222222;
}

@media (prefers-color-scheme: dark) {
    Button {
        color: #f0f0f0;
        background-color: #000000;
    }

    Button.selected {
        color: #ff0000;
    }
}
Listing 14. Example: Different font colors on Android and iOS. On Android, labels will appear green. On iOS, they will appear red. On all other platforms, they will appear black:
Label {
    color: black;
}

@media platform-and {
    Label {
        color: green;
    }
}

@media platform-ios {
    Label {
        color: red;
    }
}
Listing 15. Example: Different font colors based on device density. On lower densities, labels will be green. On higher densities, labels will be red.
Label {
    color: black;
}

@media density-very-low, density-low, density-medium, density-high {
    Label {
        color: green;
    }
}

@media density-very-high, density-h2, density-2hd, density-560 {
    Label {
        color: red;
    }
}
Listing 16. Example: Different label colors based on device type.
Label {
    color: black;
}

@media device-desktop {
    Label {
        color: green;
    }
}

@media device-tablet, device-phone {
    Label {
        color: red;
    }
}
When deploying your app using the JavaScript port, it will use a platform name derived from the "UserAgent" string in the browser, rather than the result of Display.getPlatformName(), which is used for other ports. When running on Android, then, the platform will be "and." When running on iOS, the platform will be "ios." Etc.

15.13.1. Compound media queries

You can combine multiple media queries together, separated by a comma. Queries of the same type are "OR"ed together. Queries of different types are "AND"ed together. For example if you have a media query that specifies two different device densities (for example, density-low and density-high) the query will match both devices with low density and high density. But, if the query specifies a device density and a platform (for example, density-low and platform-and), then it will match a device if it matches the platform and the density.

Listing 17. Example: Targeting styles to Android devices with high density
@media platform-and, density-high {
    Label {
        color: green;
    }
}
Listing 18. Example: Targeting styles to iOS devices with high or low density
@media platform-ios, density-high, density-low {
    Label {
        color: red;
    }
}
Listing 19. Example: Targeting Mac Desktop:
@media device-desktop, platform-mac {
    Label {
        color: blue;
    }
}

15.13.2. Order or precedence

The order of precedence when applying styles differs slightly from the way styles would be applied in standard CSS. The order of precedence is as follows:

  1. Styles defined inside @media blocks will always take precedence over styles defined outside of @media blocks.

  2. @media blocks with more query matches will take precedence over blocks with fewer query matches. For example, A media block matching density, platform, and device will take precedence over a block that only matches the density and platform.

  3. If the same style is defined in two media blocks which contain the same number of query matches, then the order precedence is platform, device, density in decreasing order. That is, the block that matches on platform will take precedence over the block that matches on density.

  4. If the same style is defined in two media blocks with identical query matches, then the order of precedence is undefined.

15.13.3. Font scaling constants

Sometimes you may find that fonts are coming out too large or too small across the board on certain types of devices. You can use standard media queries to customize font sizes, but you can also use font-scaling constants to scale font sizes for the entire stylesheet based on platform, device, and/or density. Sometimes you may find this approach easier.

For example, consider the following simple stylesheet that defines a font size of 2mm on labels:

Label {
 font-size: 3mm;
}

During testing, perhaps you find that, on desktop, the fonts are a little bit too small. In this case, you can apply a font-scale constant that only applies to the desktop:

#Constants {
 device-desktop-font-scale: "1.5";
}

Label {
 font-size: 3mm;
}

Now, on most devices the Label style will have 3mm fonts. However, on desktop, it will have 4.5mm fonts.

The above would be equivalent to:

Label {
 font-size: 3mm;
}

@media device-desktop {
 Label {
 font-size: 4.5mm;
 }
}
Listing 20. Example: Font-scaling based on device, platform, and density
#Constants {
 device-phone-font-scale: "1.5";
 device-tablet-font-scale: "1.2";
 device-desktop-font-scale: "1.4";
 platform-ios-font-scale: "0.9";
 density-low-font-scale: "1.2";
 platform-ios-density-low-font-scale: "1.3";
}
All matching font-scale constants will be applied to the styles. If you define 3 font-scale constants that all match the current runtime environment, they will all be applied. For example, If there are 3 matching font-scale constants with 2.0, 3.0, and 4.0, then fonts will be scaled by 2*3*4=24!

Graphics and games

16. Graphics, drawing, images, and fonts

Drawing is considered a low-level API that might introduce some platform fragmentation.

16.1. Basics - where and how to draw manually

The Graphics class handles drawing basics, shapes, images, and text. Developers never instantiate it directly; the Codename One API passes it in.

You can gain access to a Graphics object using one of the following methods:

  • Derive Component or a subclass of Component - Component includes many methods that let developers change drawing behavior. Override them to change how the component is drawn:

    • paint(Graphics) - invoked to draw the component, this can be overridden to draw the component from scratch.

    • paintBackground(Graphics) / paintBackgrounds(Graphics) - these allow overriding the way the component background is painted although you would probably be better off implementing a painter (see below).

    • paintBorder(Graphics) - allows overriding the process of drawing a border, notice that border drawing might differ based on the style of the component.

    • paintComponent(Graphics) - allows painting the components contents while leaving the default paint behavior to the style.

    • paintScrollbars(Graphics), paintScrollbarX(Graphics), paintScrollbarY(Graphics) allows overriding the behavior of scrollbar painting.

  • Implement the painter interface; this interface can be used as a GlassPane or a background painter.

    The painter interface is simple and includes one paint method. It lets developers perform custom painting without subclassing Component. Painters can be chained together to create elaborate paint behavior with the PainterChain class.

    • Glass pane - a glass pane allows developers to paint on top of the form painting. This allows an overlay effect on top of a form.

      At first, a glass pane may seem like overriding the Form’s paint method and drawing after super.paint(g) completes. It isn’t the same. When a component repaints by invoking repaint(), that component is drawn, and Form’s paint() method isn’t invoked. The glass pane painter is invoked in those cases and behaves as expected.

      Container has a glass-pane method called paintGlass(Graphics), which you can override to provide a similar effect at the Container level. This is useful for complex containers such as Table, which draws its lines using this approach.

    • Background painter - the background painter is installed through the style. By default, Codename One installs its own custom background painter. Installing a custom painter lets a developer define how the component background is drawn.
      Many background-style behaviors can also be achieved using styles alone.

A common mistake is overriding the paint(Graphics) method of Form. The problem is that a form has child components that may request a repaint. To avoid that, place a paintable component in the center of the Form, or override the glass pane or background painter.

A paint method can be implemented as such:

// hide the title
Form hi = new Form("", new BorderLayout());
hi.add(BorderLayout.CENTER, new Component() {
@Override
public void paint(Graphics g) {
// red color
g.setColor(0xff0000);

// paint the screen in red
g.fillRect(getX(), getY(), getWidth(), getHeight());

// draw hi world in white text in the top left corner of the screen
g.setColor(0xffffff);
g.drawString("Hi World", getX(), getY());
}
});
hi.show();
Hi world demo code under the iOS theme
Figure 207. Hi world demo code under the iOS theme; the paint fills the form and the status bar strip above it belongs to the platform

16.1.1. Painting with gradients

Solid colors are the starting point. Graphics also understands "paint" objects that describe gradients and other patterns. You can pass an instance of Paint to setColor(Paint) in place of an integer color value to activate a gradient for later fill and draw operations. The LinearGradientPaint class is the most common option and accepts a list of color stops along a line:

LinearGradientPaint gradient = new LinearGradientPaint(
        getX(), getY(), getX() + getWidth(), getY(),  // horizontal gradient
        new float[] {0f, 0.5f, 1f},
        new int[] {0xff4285f4, 0xff34a853, 0xfffbbc05},
        MultipleGradientPaint.CycleMethod.NO_CYCLE,
        MultipleGradientPaint.ColorSpaceType.SRGB,
        null
);
g.setColor(gradient);
// a Paint is consulted by fillShape; fillRect still uses the integer colour
g.fillShape(new Rectangle(getX(), getY(), getWidth(), getHeight()));

The fillLinearGradient() convenience methods (with optional repeat flag) provide a shorthand when you need a two-color gradient without constructing your own Paint object.

Radial gradients are equally straightforward using fillRadialGradient() or fillRectRadialGradient(), which can render circular and rectangular radial transitions respectively. These APIs accept the inner/outer colors, focal point, and spread so you can combine them with linear gradients to build sophisticated backgrounds and lighting effects.

For the full CSS gradient range, Graphics exposes a single fillGradient method that consumes a Gradient value object. Gradient is a Paint subclass with three concrete forms - LinearGradient, RadialGradient, ConicGradient - following the same pattern as the Shape hierarchy:

int[] colors  = { 0xffff0080, 0xffff8c00, 0xff40e0d0 };
float[] stops = { 0f, 0.5f, 1f };

// one band each, so all three are visible at once
int band = getHeight() / 3;
g.fillGradient(new LinearGradient(45f, colors, stops),
        getX(), getY(), getWidth(), band);

RadialGradient circle = new RadialGradient(colors, stops);
circle.setShape(RadialGradient.SHAPE_CIRCLE)
      .setExtent(RadialGradient.EXTENT_FARTHEST_CORNER);
g.fillGradient(circle, getX(), getY() + band, getWidth(), band);

g.fillGradient(new ConicGradient(colors, stops),
        getX(), getY() + band * 2, getWidth(), band);

Gradient carries the cycle method (CYCLE_NONE / CYCLE_REPEAT / CYCLE_REFLECT) for repeating / reflected fills. Each port implements fillGradient via the fastest native shader available - Java2D LinearGradientPaint / RadialGradientPaint on the simulator, LinearGradient / RadialGradient / SweepGradient shaders on Android - and falls back to a software rasterizer that calls Gradient#sampleArgb per pixel where no native shader exists.

16.1.2. Image blur

Graphics.gaussianBlur(Image, float radius) returns a blurred copy of an image using the platform’s fastest path (Core Image on iOS, RenderScript / RenderEffect on Android, JHLabs GaussianFilter in the simulator). The same blur is reachable from CSS via the filter: blur(<length>) and backdrop-filter: blur(<length>) properties, which are stored on the component’s Style as filterBlurRadius and backdropFilterBlurRadius.

16.2. Glass pane

The GlassPane `in Codename One is inspired by the Swing `GlassPane & LayeredPane with a few twists. You tried to imagine how Swing developers would have implemented the glass pane knowing what they do now about painters and Swings learning curve. But first: what’s the glass pane?

A typical Codename One application is essentially composed of 3 layers (this is a gross simplification though), the background painters are responsible for drawing the background of all components including the main form. The component draws its own content (which might overrule the painter) and the glass pane paints last…​

Form layout graphic
Figure 208. Form layout graphic

Essentially the glass pane is a painter that allows you to draw an overlay on top of the Codename One application.

Overriding the paint method of a form isn’t a substitute for glasspane as it would appear to work initially, when you enter a Form. For example, when modifying an element within the form that element gets repainted not the entire Form!

If you have a form with a Button and text drawn on top using the Form’s paint method it would get erased whenever the button gets focus.

The glass pane is called whenever a component gets painted, it paints within the clipping region of the component hence it won’t break the rest of the components on the Form which weren’t modified.

You can set a painter on a form using code like this:

hi.setGlassPane(new Painter() {
    @Override
    public void paint(Graphics g, Rectangle rect) {
    }
});

Or you can use Java 8 lambdas to tighten the code a bit:

hi.setGlassPane((g, rect) -> {
});

PainterChain allows you to chain many painters together to perform different logical tasks such as a validation painter coupled with a fade out painter. The sample below shows a crude validation panel that allows you to draw error icons next to components while exceeding their physical bounds as is common in many user interfaces

Form hi = new Form("Glass Pane", new BoxLayout(BoxLayout.Y_AXIS));
Style s = UIManager.getInstance().getComponentStyle("Label");
s.setFgColor(0xff0000);
s.setBgTransparency(0);
Image warningImage = FontImage.createMaterial(FontImage.MATERIAL_WARNING, s).toImage();
TextField tf1 = new TextField("My Field");
tf1.getAllStyles().setMarginUnit(Style.UNIT_TYPE_DIPS);
tf1.getAllStyles().setMargin(5, 5, 5, 5);
hi.add(tf1);
hi.setGlassPane((g, rect) -> {
int x = tf1.getAbsoluteX() + tf1.getWidth();
int y = tf1.getAbsoluteY();
x -= warningImage.getWidth() / 2;
y += (tf1.getHeight() / 2 - warningImage.getHeight() / 2);
g.drawImage(warningImage, x, y);
});
hi.show();
The glass pane draws the warning sign on the border of the component peeking out
Figure 209. The glass pane draws the warning sign on the border of the component peeking out

16.3. Shapes & transforms

The graphics API provides a high performance shape API that allows drawing arbitrary shapes by defining paths and curves and caching the shape drawn in the GPU.

16.4. Device support

Shapes and transforms ship with all Codename One’s actively maintained ports (Android, iOS, JavaScript, and desktop/Simulator). Older platforms that have reached end of life may lack these APIs, so keep the guard code shown below if you still target them with legacy builds.

Notice that perspective transform is missing from the desktop/simulator port. No real equivalent to perspective transform in Java SE that you could use.

16.5. A 2D drawing app

You can show shape drawing with a simple example of a drawing app, that allows the user to tap the screen to draw a contour picture.

The app works by keeping a GeneralPath in memory, and adding points as Bézier curves. Whenever a point is added, the path is redrawn to the screen.

The center of the app is the DrawingCanvas class, which extends Component:

public class DrawingCanvas extends Component {
    GeneralPath p = new GeneralPath();
    int strokeColor = 0x0000ff;
    int strokeWidth = 10;

    public void addPoint(float x, float y){
        // To be written
    }

    @Override
    protected void paintBackground(Graphics g) {
        super.paintBackground(g);
            Stroke stroke = new Stroke(
                strokeWidth,
                Stroke.CAP_BUTT,
                Stroke.JOIN_ROUND, 1f
            );
            g.setColor(strokeColor);

            // Draw the shape
            g.drawShape(p, stroke);

    }

    @Override
    public void pointerPressed(int x, int y) {
        addPoint(x-getParent().getAbsoluteX(), y-getParent().getAbsoluteY());
    }
}

Conceptually this is basic component. You will be overriding the paintBackground() method to draw the path. You keep a reference to a GeneralPath object (which is the concrete implementation of the Shape interface in Codename One) to store each successive point in the drawing. You also parametrize the stroke width and color.

The implementation of the paintBackground() method (shown above) should be straight forward. It creates a stroke of the appropriate width, and sets the color on the graphics context. Then it calls drawShape() to render the path of points.

16.5.1. Implementing addpoint()

The addPoint method is designed to allow you to add points to the drawing. A simple implementation that uses straight lines rather than curves might look like this:

private float lastX = -1;
private float lastY = -1;

public void addPoint(float x, float y) {
    if (lastX == -1) {
        // this is the first point... Don't draw a line yet
        p.moveTo(x, y);
    } else {
        p.lineTo(x, y);
    }
    lastX = x;
    lastY = y;

    repaint();
}

The sample introduces a couple of house-keeping member vars (lastX and lastY) to store the last point that was added that you know whether this is the first tap or a later tap. The first tap triggers a moveTo() call, whereas later taps trigger lineTo() calls, which draw lines from the last point to the current point.

A drawing might look like this:

lineTo example
Figure 210. lineTo example

16.5.2. Using Bézier curves

Your previous implementation of addPoint() used lines for each segment of the drawing. Now make a change to allow for smoother edges by using quadratic curves instead of lines.

Codename One’s GeneralPath class includes two methods for drawing curves:

  1. quadTo() : Appends a quadratic Bézier curve. It takes 2 points: a control point, and an end point.

  2. curveTo() : Appends a cubic Bézier curve, taking 3 points: 2 control points, and an end point.

See the General Path javadocs for the full API.

You will make use of the quadTo() method to append curves to the drawing as follows:

private boolean odd=true;
public void addPoint(float x, float y){
    if ( lastX == -1 ){
        p.moveTo(x, y);

    } else {
        float controlX = odd ? lastX : x;
        float controlY = odd ? y : lastY;
        p.quadTo(controlX, controlY, x, y);
    }
    odd = !odd;
    lastX = x;
    lastY = y;
    repaint();
}

This change should be straight forward except, perhaps, the business with the odd variable. Since quadratic curves require two points (also to the implied starting point), you can’t take the last tap point and the current tap point. You need a point between them to act as a control point. This is where you get the curve from. The control point works by exerting a sort of "gravity" on the line segment, to pull the line towards it. This results in the line being curved. The example uses the odd marker to alternate the control point between positions above the line and below the line.

A drawing from the resulting app looks like:

Result of quadTo example
Figure 211. Result of quadTo example

16.5.3. Detecting platform support

The DrawingCanvas example is a bit naive in that it assumes that the device supports the shape API. Running this code on a device that doesn’t support the Shape API would draw a blank canvas where the shape was expected to be drawn. You can fall back gracefully if you make use of the Graphics.isShapeSupported() method. For example:

@Override
protected void paintBackground(Graphics g) {
    super.paintBackground(g);
    if ( g.isShapeSupported() ){
       // do my shape drawing code here
    } else {
        // draw an alternate representation for device
        // that doesn't support shapes.
        // E.g. You could defer to the Pisces
        // library in this case
    }

}

16.6. Transforms

The Graphics class has included limited support for 2D transformations for some time now including scaling, rotation, and translation:

  • scale(x,y) : Scales drawing operations by a factor in each direction.

  • translate(x,y) : Translates drawing operations by an offset in each direction.

  • rotate(angle) : Rotates about the origin.

  • rotate(angle, px, py) : Rotates about a pivot point.

scale() and rotate() methods are available on platforms that support Affine transforms. See table X for a compatibility list.

16.6.1. Device support

All current Codename One ports expose affine transforms (that’s: scale() and rotate()). Use the following table as a quick reference when deciding whether you need a fallback path.

Table 10. Transforms Device Support

Platform

Affine Supported

Simulator/Desktop

Yes

iOS

Yes

Android

Yes

JavaScript

Yes

You can check if a particular Graphics context supports rotation and scaling using the isAffineSupported() method.

For example:

public void paint(Graphics g){
    if ( g.isAffineSupported() ){
        // Do something that requires rotation and scaling

    } else {
        // Fallback behavior here
    }
}

16.7. Example: Drawing an analog clock

The following sections implement an analog clock component. This will show three key concepts in Codename One’s graphics:

  1. Using the GeneralPath class for drawing arbitrary shapes.

  2. Using Graphics.translate() to translate your drawing position by an offset.

  3. Using Graphics.rotate() to rotate your drawing position.

There are three separate things that need to be drawn in a clock:

  1. The tick marks. For example: most clocks will have a tick mark for each second, larger tick marks for each hour, and sometimes even larger tick marks for each quarter-hour.

  2. The numbers. You will draw the clock numbers (1 through 12) in the appropriate positions.

  3. The hands. You will draw the clock hands to point at the appropriate points to display the current time.

16.7.1. The AnalogClock Component

Your clock will extend the Component class, and override the paintBackground() method to draw the clock as follows:

public class AnalogClock extends Component {
    Date currentTime = new Date();

    @Override
    public void paintBackground(Graphics g) {
        // Draw the clock in this method
    }
}

16.7.2. Setting up the parameters

Before you actually draw anything, take a moment to figure out what values you need to know to draw an effective clock. Minimally, you need two values:

  1. The center point of the clock.

  2. The radius of the clock.

The clock also exposes the following parameters to help customize how it’s rendered:

  1. The padding (that’s: the space between the edge of the component and the edge of the clock circle.

  2. The tick lengths. The example uses 3 different lengths of tick marks on this clock. The longest ticks will be displayed at quarter points (that’s: 12, 3, 6, and 9). Slightly shorter ticks will be displayed at the five-minute marks (that’s: where the numbers appear), and the remaining marks (corresponding with seconds) will be short:

// Hard code the padding at 10 pixels for now
double padding = 10;

// Clock radius
double r = Math.min(getWidth(), getHeight())/2-padding;

// Center point.
double cX = getX()+getWidth()/2;
double cY = getY()+getHeight()/2;

//Tick Styles
int tickLen = 10;  // short tick
int medTickLen = 30;  // at 5-minute intervals
int longTickLen = 50; // at the quarters
int tickColor = 0xCCCCCC;
Stroke tickStroke = new Stroke(2f, Stroke.CAP_BUTT, Stroke.JOIN_ROUND, 1f);

16.7.3. Drawing the tick marks

For the tick marks, you will use a single GeneralPath object, making use of the moveTo() and lineTo() methods to draw each individual tick:

// a fresh path each repaint, otherwise the segments accumulate
GeneralPath ticksPath = new GeneralPath();
        // Draw a tick for each "second" (1 through 60)
for ( int i=1; i<= 60; i++){
    // default tick length is short
    int len = tickLen;
    if ( i % 15 == 0 ){
        // Longest tick on quarters (every 15 ticks)
        len = longTickLen;
    } else if ( i % 5 == 0 ){
        // Medium ticks on the '5's (every 5 ticks)
        len = medTickLen;
    }

    double di = (double)i; // tick num as double for easier math

    // Get the angle from 12 O'Clock to this tick (radians)
    double angleFrom12 = di/60.0*2.0*Math.PI;

    // Get the angle from 3 O'Clock to this tick
        // Note: 3 O'Clock corresponds with zero angle in unit circle
        // Makes it easier to do the math.
    double angleFrom3 = Math.PI/2.0-angleFrom12;

    // Move to the outer edge of the circle at correct position
    // for this tick.
    ticksPath.moveTo(
            (float)(cX+Math.cos(angleFrom3)*r),
            (float)(cY-Math.sin(angleFrom3)*r)
    );

    // Draw line inward along radius for length of tick mark
    ticksPath.lineTo(
            (float)(cX+Math.cos(angleFrom3)*(r-len)),
            (float)(cY-Math.sin(angleFrom3)*(r-len))
    );
}

// Draw the full shape onto the graphics context.
g.setColor(tickColor);
g.drawShape(ticksPath, tickStroke);
This example uses a little bit of trigonometry to calculate the (x,y) coordinates of the tick marks based on the angle and the radius. If math isn’t your thing, don’t worry. This example makes use of the identities: x=r*cosθ and y=r*sinθ.

At this point your clock should include a series of tick marks orbiting a blank center as shown below:

Drawing tick marks on the watch face
Figure 212. Drawing tick marks on the watch face

16.7.4. Drawing the numbers

The Graphics.drawString(str, x, y) method allows you to draw text at any point of a component. The tricky part here is calculating the correct x and y values for each string so that the number appears in the correct location.

For the purposes of this tutorial, you will use the following strategy. For each number (1 through 12):

  1. Use the Graphics.translate(x,y) method to apply a translation from the clock’s center point to the point where the number should appear.

  2. Draw number (using drawString()) at the clock’s center. It should be rendered at the correct point due to your translation.

  3. Invert the translation performed in step 1:

for ( int i=1; i<=12; i++){
    // Calculate the string width and height so we can center it properly
    String numStr = ""+i;
    int charWidth = g.getFont().stringWidth(numStr);
    int charHeight = g.getFont().getHeight();

    double di = (double)i;  // number as double for easier math

    // Calculate the position along the edge of the clock where the number should
    // be drawn
     // Get the angle from 12 O'Clock to this tick (radians)
    double angleFrom12 = di/12.0*2.0*Math.PI;

    // Get the angle from 3 O'Clock to this tick
        // Note: 3 O'Clock corresponds with zero angle in unit circle
        // Makes it easier to do the math.
    double angleFrom3 = Math.PI/2.0-angleFrom12;

    // Get diff between number position and clock center
    int tx = (int)(Math.cos(angleFrom3)*(r-longTickLen));
    int ty = (int)(-Math.sin(angleFrom3)*(r-longTickLen));

    // For 6 and 12 we will shift number slightly so they're more even
    if ( i == 6 ){
        ty -= charHeight/2;
    } else if ( i == 12 ){
        ty += charHeight/2;
    }

    // Translate the graphics context by delta between clock center and
    // number position
    g.translate(
            tx,
            ty
    );


    // Draw number at clock center.
    g.drawString(numStr, (int)cX-charWidth/2, (int)cY-charHeight/2);

    // Undo translation
    g.translate(-tx, -ty);

}
This example is, admittedly, a little contrived to allow for a demonstration of the Graphics.translate() method. You could have as passed the exact location of the number to drawString() rather than draw at the clock center and translate to the correct location.

Now, you should have a clock with tick marks and numbers as shown below:

Drawing the numbers on the watch face
Figure 213. Drawing the numbers on the watch face

16.7.5. Drawing the hands

The clock will include three hands: Hour, Minute, and Second. You will use a separate GeneralPath object for each hand. For the positioning/angle of each, use the following strategy:

  1. Draw the hand at the clock center pointing toward 12 (straight up).

  2. Translate the hand slightly down so that it overlaps the center.

  3. Rotate the hand at the appropriate angle for the current time, using the clock center as a pivot point.

Drawing the Second Hand:

For the "second" hand, you will use a simple line from the clock center to the inside edge of the medium tick mark at the 12 o’clock position:

GeneralPath secondHand = new GeneralPath();
secondHand.moveTo((float)cX, (float)cY);
secondHand.lineTo((float)cX, (float)(cY-(r-medTickLen)));

And you will translate it down slightly so that it overlaps the center. This translation will be performed on the GeneralPath object directly rather than through the Graphics context:

Shape translatedSecondHand = secondHand.createTransformedShape(
    Transform.makeTranslation(0f, 5)
);

Rotating the Second Hand::

The rotation of the second hand will be performed in the Graphics context through the rotate(angle, px, py) method. This requires you to calculate the angle. The px and py arguments constitute the pivot point of the rotation, which, in your case will be the clock center.

The rotation pivot point is expected to be in absolute screen coordinates rather than relative coordinates of the component. You therefore need to get the absolute clock center position to perform the rotation:
// Calculate the angle of the second hand
Calendar calendar = Calendar.getInstance(TimeZone.getDefault());
double second = (double)(calendar.get(Calendar.SECOND));
double secondAngle = second/60.0*2.0*Math.PI;

// Get absolute center position of the clock
double absCX = getAbsoluteX()+cX-getX();
double absCY = getAbsoluteY()+cY-getY();

// keep whatever transform the caller had, rather than resetting to none
Transform originalTransform = g.getTransform();
g.rotate((float)secondAngle, (int)absCX, (int)absCY);
g.setColor(0xff0000);
g.drawShape(
        translatedSecondHand,
        new Stroke(2f, Stroke.CAP_BUTT, Stroke.JOIN_BEVEL, 1f)
);
g.setTransform(originalTransform);
Put the transform back when you’re done with the rotation, or the drawing that follows uses your rotated coordinate system. Save it with getTransform() first and restore it with setTransform(), as the sample above does. resetAffine() looks like the way to do this and isn’t: it clears the transform entirely, including one a parent set before your component was painted.

Drawing the Minute And Hour Hands:

The mechanism for drawing the hour and minute hands is largely the same as for the minute hand, with a couple of added complexities though:

  1. You will make these hands trapezoidal, and almost triangular rather than using a simple line, which means the GeneralPath construction will be slightly more complex.

  2. Calculation of the angles will be slightly more complex because they need to consider many parameters. For example: The hour hand angle is informed by both the hour of the day and the minute of the hour.

The remaining drawing code is as follows:

// Draw the minute hand
GeneralPath minuteHand = new GeneralPath();
minuteHand.moveTo((float)cX, (float)cY);
minuteHand.lineTo((float)cX+6, (float)cY);
minuteHand.lineTo((float)cX+2, (float)(cY-(r-tickLen)));
minuteHand.lineTo((float)cX-2, (float)(cY-(r-tickLen)));
minuteHand.lineTo((float)cX-6, (float)cY);
minuteHand.closePath();

// Translate the minute hand slightly down so it overlaps the center
Shape translatedMinuteHand = minuteHand.createTransformedShape(
    Transform.makeTranslation(0f, 5)
);

double minute = (double)(calendar.get(Calendar.MINUTE)) +
        (double)(calendar.get(Calendar.SECOND))/60.0;

double minuteAngle = minute/60.0*2.0*Math.PI;

// Rotate and draw the minute hand, keeping the caller's transform
Transform beforeMinuteHand = g.getTransform();
g.rotate((float)minuteAngle, (int)absCX, (int)absCY);
g.setColor(0x000000);
g.fillShape(translatedMinuteHand);
g.setTransform(beforeMinuteHand);


// Draw the hour hand
GeneralPath hourHand = new GeneralPath();
hourHand.moveTo((float)cX, (float)cY);
hourHand.lineTo((float)cX+4, (float)cY);
hourHand.lineTo((float)cX+1, (float)(cY-(r-longTickLen)*0.75));
hourHand.lineTo((float)cX-1, (float)(cY-(r-longTickLen)*0.75));
hourHand.lineTo((float)cX-4, (float)cY);
hourHand.closePath();

Shape translatedHourHand = hourHand.createTransformedShape(
    Transform.makeTranslation(0f, 5)
);

//Calendar cal = Calendar.getInstance().get
double hour = (double)(calendar.get(Calendar.HOUR_OF_DAY)%12) +
        (double)(calendar.get(Calendar.MINUTE))/60.0;

double angle = hour/12.0*2.0*Math.PI;
Transform beforeHourHand = g.getTransform();
g.rotate((float)angle, (int)absCX, (int)absCY);
g.setColor(0x000000);
g.fillShape(translatedHourHand);
g.setTransform(beforeHourHand);

16.7.6. The final result

At this point, you have a complete clock as shown below:

The final result - fully rendered watch face
Figure 214. The final result - fully rendered watch face

16.7.7. Animating the clock

The current clock component is cool, but it’s static. It displays the time at the point the clock was created. You discussed low-level animations in the animation section of the guide, here you will show a somewhat more elaborate example.

To animate your clock so that it updates once per second, you need to do two things:

  1. Implement the animate() method to show when the clock needs to be updated/re-drawn.

  2. Register the component with the form so that it will receive animation "pulses."

The animate() method in the AnalogClock class:

long lastRenderedTime = 0;

@Override
public boolean animate() {
    long now = System.currentTimeMillis();
    if ( now/1000 != lastRenderedTime/1000){
        currentTime.setTime(now);
        lastRenderedTime = now;
        return true;
    }
    return false;
}

This method will be invoked on each "pulse" of the EDT. It checks the last time the clock was rendered and returns true if the clock hasn’t been rendered in the current "time second" interval. Otherwise it returns false. This ensures that the clock will be redrawn when the time changes.

16.8. Starting and stopping the animation

Animations can be started and stopped through the Form.registerAnimated(component) and Form.deregisterAnimated(component) methods. The sample encapsulates these calls in start() and stop() methods in the component as follows:

public void start(){
    getComponentForm().registerAnimated(this);
}

public void stop(){
    getComponentForm().deregisterAnimated(this);
}

The code to instantiate the clock, and start the animation would be something like:

AnalogClock clock = new AnalogClock();
// the clock draws to its own bounds and declares no preferred size, so it
// needs a region that stretches -- in a flow or box layout it would be 0x0
Form hi = new Form("Clock", new BorderLayout());
hi.add(BorderLayout.CENTER, clock);
hi.show();
clock.start();

16.9. Shape clipping

Clipping is one of the core tenants of graphics programming, you define the boundaries for drawing and when you exceed said boundaries things aren’t drawn. Shape clipping allows you to clip based on any arbitrary Shape and not a rectangle, this allows some unique effects generated in runtime.

For example: this code allows you to draw a rather complex image of duke:

Image duke = null;
try {
    // duke.png is just the default Codename One icon copied into place
    duke = Image.createImage("/duke.png");
} catch(IOException err) {
    Log.e(err);
}
final Image finalDuke = duke;

Form hi = new Form("Shape Clip");

// We create a 50 x 100 shape, this is arbitrary since we can scale it easily
GeneralPath path = new GeneralPath();
path.moveTo(20,0);
path.lineTo(30, 0);
path.lineTo(30, 100);
path.lineTo(20, 100);
path.lineTo(20, 15);
path.lineTo(5, 40);
path.lineTo(5, 25);
path.lineTo(20,0);

Stroke stroke = new Stroke(0.5f, Stroke.CAP_ROUND, Stroke.JOIN_ROUND, 4);
hi.getContentPane().getUnselectedStyle().setBgPainter((Graphics g, Rectangle rect) -> {
    g.setColor(0xff);
    float widthRatio = ((float)rect.getWidth()) / 50f;
    float heightRatio = ((float)rect.getHeight()) / 100f;
    g.scale(widthRatio, heightRatio);
    g.translate((int)(((float)rect.getX()) / widthRatio), (int)(((float)rect.getY()) / heightRatio));
    g.setClip(path);
    g.setAntiAliased(true);
    g.drawImage(finalDuke, 0, 0, 50, 100);
    g.setClip(path.getBounds());
    g.drawShape(path, stroke);
    g.translate(-(int)(((float)rect.getX()) / widthRatio), -(int)(((float)rect.getY()) / heightRatio));
    g.resetAffine();
});

hi.show();
Shape Clipping used to clip the image of duke within the given shape
Figure 215. Shape Clipping used to clip the image of duke within the given shape
Notice that this functionality isn’t available on all platforms so you need to test if shaped clipping is supported using isShapeClipSupported().

16.10. The coordinate system

The Codename One coordinate system follows the example of Swing (and many other - but not all-graphics libraries) and places the origin in the upper left corner of the screen. X-values grow to the right, and Y-values grow downward as illustrated below:

The Codename One graphics coordinate space
Figure 216. The Codename One graphics coordinate space

The screen origin is in the top left corner of the screen. Given this information, consider the method call on the Graphics context g:

g.drawRect(10,10, 100, 100);

Where would this rectangle be drawn on the screen?

If you answered something like "10 pixels from the top, and 10 pixels from the left of the screen," you might be right. It depends on whether the graphics has a translation or transform applied to it. If there is currently a translation of (20,20) (that’s: 20 pixels to the right, and 20 pixels down), then the rectangle would be rendered at (30, 30).

You can always find out the current translation of the graphics context using the Graphics.getTranslateX() and Graphics.getTranslateY() methods:

// Find out the current translation
int currX = g.getTranslateX();
int currY = g.getTranslateY();

// Reset the translation to zeroes
g.translate(-currX, -currY);

// Now we are working in absolute screen coordinates
g.drawRect(10, 10, 100, 100);

// This rectangle should now be drawn at the exact screen
// coordinates (10,10).

//Restore the translation
g.translate(currX, currY);
This example glosses over issues such as clipping and transforms which may cause it to not work as you expect. For example: When painting a component inside its paint() method, there is a clip applied to the context so that the content you draw within the bounds of the component will be seen.

If, also, there is a transform applied that rotates the context 45 degrees clockwise, then the rectangle will be drawn at a 45-degree angle with its top left corner somewhere on the left edge of the screen.

You don’t have to worry about the exact screen coordinates for the things you paint. Most of the time, you will be concerned with relative coordinates.

16.10.1. Relative coordinates

Usually, when you’re drawing onto a Graphics context, you’re doing so within the context of a Component’s paint() method (or one of its variants). In this case, you don’t care what the exact screen coordinates are of your drawing. You’re concerned with their relative location within the coordinate. You can leave the positioning (and even sizing) of the coordinate up to Codename One. Thank you for reading.

To show this, create a simple component called Rectangle component, that draws a rectangle on the screen. You will use the component’s position and size to dictate the size of the rectangle to be drawn. And you will keep a 5 pixel padding between the edge of the component and the edge of your rectangle:

class RectangleComponent extends Component {
    public void paint(Graphics g){
        g.setColor(0x0000ff);
        g.drawRect(getX()+5, getY()+5, getWidth()-10, getHeight()-10);
    }
}

The result is as follows:

The rectangle component
Figure 217. The rectangle component
The x and y coordinates that are passed to the drawRect(x,y,w,h) method are relative to the component’s parent’s origin — not the component itself.. Its parent. This is why you the x position is getX()+5 and not 5.

16.10.2. Transforms and rotations

Unlike the Graphics drawXXX primitives, methods for setting transformations, including scale(x,y) and rotate(angle), are always applied in screen coordinates. This can be confusing at first, because you may be unsure whether to provide a relative coordinate or an absolute coordinate for a given method.

The general rule is:

  1. All coordinates passed to the drawXXX() and fillXXX() methods will be subject to the graphics context’s transform and translation settings.

  2. All coordinates passed to the context’s transformation settings are considered to be screen coordinates, and aren’t subject to current transform and translation settings.

Take your RectangleComponent as an example. Suppose you want to rotate the rectangle by 45 degrees, your first try might look something like:

class RectangleComponent extends Component {

        @Override
        protected Dimension calcPreferredSize() {
            return new Dimension(250,250);
        }

        public void paint(Graphics g) {
            g.setColor(0x0000ff);
            g.rotate((float) (Math.PI / 4.0));
            g.drawRect(getX() + 5, getY() + 5, getWidth() - 10, getHeight() - 10);
            g.rotate(-(float) (Math.PI / 4.0));
        }
    }
When performing rotations and transformations inside a paint() method, always remember to revert your transformations at the end of the method so that it doesn’t pollute the rendering pipeline for later components.

The behavior of this rotation will vary based on where the component is rendered on the screen. To show this, try to place five of these components on a form inside a BorderLayout and see how it looks:

class MyForm extends Form {

    public MyForm() {
        super("Rectangle Rotations", new BorderLayout());
        // one per position, so the rotation can be compared across the screen
        add(BorderLayout.NORTH, new RectangleComponent());
        add(BorderLayout.SOUTH, new RectangleComponent());
        add(BorderLayout.EAST, new RectangleComponent());
        add(BorderLayout.WEST, new RectangleComponent());
        add(BorderLayout.CENTER, new RectangleComponent());
    }
}
new MyForm().show();

The result is as follows:

Rotating the rectangle
Figure 218. Rotating the rectangle

This may not be an intuitive outcome since you drew five rectangle components, but you see a portion of one rectangle. The reason is that the rotate(angle) method uses the screen origin as the pivot point for the rotation. Components nearer to this pivot point will experience a less dramatic effect than components farther from it. In your case, the rotation has caused all rectangles except the first one to be rotated outside the bounds of their containing component, and are therefore being clipped. A more sensible solution for your component would be to place the rotation pivot point somewhere inside the component. That way all the components would look the same. Some possibilities would be:

Top Left Corner:

public void paint(Graphics g) {
    g.setColor(0x0000ff);
    g.rotate((float)(Math.PI/4.0), getAbsoluteX(), getAbsoluteY());
    g.drawRect(getX() + 5, getY() + 5, getWidth() - 10, getHeight() - 10);
    g.rotate(-(float) (Math.PI / 4.0), getAbsoluteX(), getAbsoluteY());
}
Rotating the rectangle with wrong pivot point
Figure 219. Rotating the rectangle with wrong pivot point

Center:

public void paint(Graphics g) {
    g.setColor(0x0000ff);
    g.rotate(
        (float)(Math.PI/4.0),
        getAbsoluteX()+getWidth()/2,
        getAbsoluteY()+getHeight()/2
    );
    g.drawRect(getX() + 5, getY() + 5, getWidth() - 10, getHeight() - 10);
    g.rotate(
        -(float)(Math.PI/4.0),
        getAbsoluteX()+getWidth()/2,
        getAbsoluteY()+getHeight()/2
    );
}
Rotating the rectangle with the center pivot point
Figure 220. Rotating the rectangle with the center pivot point

You could also use the Graphics.setTransform() class to apply rotations and other complex transformations (including 3D perspective transforms), but that belongs to its own topic as it’s a little bit more complex.

16.10.3. Global alpha & Anti-Aliasing

Up to this point you’ve relied on the per-pixel alpha stored in images and gradients. Graphics also lets you apply a global alpha multiplier to every draw call by using setAlpha(int) or concatenateAlpha(int) after checking isAlphaSupported(). Both methods accept values from 0 (fully transparent) to 255 (fully opaque) and remain active until you change them again. concatenateAlpha() is especially handy when you need to temporarily fade a component because it returns the previous alpha so you can restore it later.

Anti-aliasing can likewise be toggled at runtime. Call isAntiAliasingSupported() and isAntiAliasedTextSupported() to discover which hints the current port exposes, then use setAntiAliased(boolean) and setAntiAliasedText(boolean) to opt into smoother edges for shapes and glyphs respectively. These switches make it easy to balance rendering quality versus speed depending on the type of content you draw.

16.10.4. Event coordinates

The coordinate system and event handling are tied. You can listen for touch events on a component by overriding the pointerPressed(x,y) method. The coordinates received in this method will be absolute screen coordinates, so you may need to do some conversions on these coordinates before using them in your drawXXX() methods.

For example: a pointerPressed() callback method can look like this:

public void pointerPressed(int x, int y) {
    addPoint(x-getParent().getAbsoluteX(), y-getParent().getAbsoluteY());
}

In this case you translated these points so that they would be relative to the origin of the parent component. This is because the drawXXX() methods for this component take coordinates relative to the parent component.

16.11. Images

Codename One has a few image types: loaded, RGB (built-in), RGB (Codename One), Mutable, EncodedImage, SVG, MultiImage, FontImage & Timeline. URLImage, FileEncodedImage, FileEncodedImageAsync, and StorageEncodedImage/Async also exist, and are covered in the IO section.

All image types are seamless to use and will work with drawImage and various image related image APIs for the most part with caveats on performance etc.

For animation images the code must invoke the animate() method on the image (this is done automatically by Codename One when placing the image as a background or as an icon!
You need to do it if you invoke drawImage in code rather than use a built-in component).

Performance and memory wise you should read the section below and be aware of the image types you use. The Codename One designer tries to conserve memory and be "clever" by using EncodedImage. While these are great for low memory you need to understand the complexities of image locking and be aware that you might pay a penalty if you don’t.

Here are the pros/cons and logic behind every image type. This covers the logic of how it’s created:

16.11.1. Loaded Image

This is the basic image you get when loading an image from the jar or network using Image.createImage(String), Image.createImage(InputStream) & Image.createImage(byte array, int, int),…​

Some other APIs might return this image type but those APIs do so explicitly!

In some platforms calling getGraphics() on an image like this will throw an exception as it’s immutable). This is true for most other images as well.

This restriction might not apply for all platforms.

The image is stored in RAM based on device logic and should be reasonably efficient in drawing speed. For example, it takes up a lot of RAM.

To calculate the amount of RAM taken by a loaded image you use the following formula:

Image Width * Image Height * 4 = Size In RAM in Bytes

For example: a 50×100 image will take up 20,000 bytes of RAM.

The logic behind this is simple, every pixel contains 3 color channels and an alpha component hence 3 bytes for color and one for alpha.

This isn’t the case for all images but it’s common and you prefer calculating for the worst case scenario. Even with JPEGs that don’t include an alpha channel some OSes might require that more byte.

16.11.2. The RGB image’s

There are two types of RGB constructed images that are different from one another but since they’re both technically "RGB image’s" you’re bundling them under the same subsection.

Internal

This is a close cousin of the loaded image. This image is created using the method Image.createImage(int array, int, int) and receives the AARRGGBB data to form the image. It’s more efficient than the Codename One RGB image but can’t be modified, at least not on the pixel level.

The goal of this image type is to provide an easy way to render RGB data that isn’t modified efficiently at platform native speeds. It’s technically a standard "Loaded Image" internally.

RGBImage class

RGBImage is effectively an AARRGGBB array that can be drawn by Codename One.

On most platforms this is inefficient but for some pixel level manipulations there is no other way.

An RGBImage is constructed with an int array (int[]) that includes width*height elements. You can then change the colors and alpha channel directly within the array and draw the image to any source using standard image drawing APIs.

This is inefficient in rendering speed and memory overhead. Only use this technique if there is absolutely no other way!

16.11.3. EncodedImage

EncodedImage is the workhorse of Codename One. Images returned from resource files are EncodedImage and many APIs expect it.

The EncodedImage is effectively a loaded image that’s "hidden" and extracted as needed to remove the memory overhead associated with loaded image. When creating an EncodedImage the PNG (or JPEG etc.) is loaded to an array in RAM. Such images are small (relatively) so they can be kept in memory without much overhead.

When image information is needed (pixels) the image is decoded into RAM and kept in a weak/sort reference. This allows the image to be cached for performance and allows the garbage collector to reclaim it when the memory becomes scarce.

Since the fully decoded image can be pretty big (width X height X 4) the ability to store the encoded image can be pretty stark. For example: taking your example above a 50×100 image will take up 20,000 bytes of RAM for a loaded image but an EncodedImage can reduce that to 1kb-2kb of RAM.

An EncodedImage might be more expensive than a loaded image as it will take up both the encoded size and the loaded size. The cost might be slightly bigger sometimes. It’s main value is its ability to shrink.

When drawing an EncodedImage it checks the weak reference cache and if the image is cached then it’s shown otherwise the image is loaded the encoded image cache it then drawn.

EncodedImage isn’t final and can be derived to produce complex image fetching strategies for example: the URLImage class that can dynamically download its content from the web.

EncodedImage can be instantiated through the create methods in the EncodedImage class. Pretty much any image can be converted into an EncodedImage through the createFromImage(Image, boolean) method.

EncodedImage Locking

Loading the image is more expensive so you want the images that are on the current form to remain in cache (otherwise GC will thrash a lot). That’s where lock() kicks in, when lock() is active you keep a hard reference to the actual native image so it won’t get GC’d. This significantly improves performance!

Internally this is invoked automatically for background images, icons etc. which results in a huge performance boost. This makes sense since these images are showing and they will be in RAM anyway. But, if you use a complex renderer or custom drawing UI you should lock() your images where possible!

To verify that locking might be a problem you can launch the performance monitor tool (accessible from the simulator menu), if you get log messages that show that an unlocked image was drawn you might have a problem.

16.11.4. MultiImage

Multi images don’t physically exist as a concept within the Codename One API so there is no way to actually create them and they’re in no way distinguishable from EncodedImage.

The built-in support for multi images is in the resource file loading logic where a MultiImage is decoded and the version that matches the current DPI is physically loaded. From that point on user code can treat it like any other EncodedImage.

The 9-image borders use multi images by default to keep their appearance more refined on the different DPI’s.

16.11.5. FontImage & Material design icons

FontImage allows using an icon font as if it was an image. You can specify the character, color and size and then treat the FontImage as if its a regular image. The huge benefits are that the font image can adapt to platform conventions in color and scale to adapt to DPI.

You can generate icon fonts using free tools on the internet such as this. Icon fonts are a simple and powerful technique to create a small, modern applications.

Icon fonts can be created in 2 basic ways the first is explicitly by defining all the elements within the font:

Form hi = new Form("Icon Font");
Font materialFont = FontImage.getMaterialDesignFont();
int w = Display.getInstance().getDisplayWidth();
FontImage fntImage = FontImage.createFixed("\uE161", materialFont, 0xff0000, w, w);
hi.add(fntImage);
hi.show();
Icon font from material design icons created with the fixed size of display width
Figure 221. Icon font from material design icons created with the fixed size of display width
The samples use the built-in material design icon font. This is for convenience so the sample will work out of the box, for everyone. For example you should be able to do this with any arbitrary icon font off the internet as long as its a valid TTF file.

A more common and arguably "correct" way to construct such an icon would be through the Style object. The Style object can provide the color, size and background information needed by FontImage.

There are two versions of this method: the first one expects the Style object to have the correct icon font set to its font attribute. The second accepts a Font object as an argument. The latter is useful for a case where you want to reuse the same Style object that you defined for a general UI element for example: you can set an icon for a Button like this and it will take up the style of the Button:

Form hi = new Form("Icon Font");
Font materialFont = FontImage.getMaterialDesignFont();
int size = Display.getInstance().convertToPixels(6, true);
materialFont = materialFont.derive(size, Font.STYLE_PLAIN);
Button myButton = new Button("Save");
myButton.setIcon(FontImage.create("\uE161", myButton.getUnselectedStyle(), materialFont));
hi.add(myButton);
hi.show();
An image created from the Style object
Figure 222. An image created from the Style object
Notice that for this specific version of the method the size of the font is used to determine the icon size. In the other methods for FontImage creation the size of the font is ignored!
Material design icons

There are many icon fonts on the web, but the field is rather volatile and constantly changing. For example, you wanted to have built-in icons that would allow you to create better looking demos and built-in components.

That’s why you picked the material design icon font for inclusion in the Codename One distribution. It features a stable core set of icons, that aren’t IP encumbered.

You can use the built-in font directly as demonstrated above but there are far better ways to create a material design icon. To find the icon you want you can check out the material design icon gallery. For example: you used the save icon in the samples above.

To recreate the save icon from above you can do something like:

Form hi = new Form("Icon Font");
Button myButton = new Button("Save");
myButton.setIcon(FontImage.createMaterial(FontImage.MATERIAL_SAVE, myButton.getUnselectedStyle()));
hi.add(myButton);
Material save icon
Figure 223. Material save icon
Notice that the icon is smaller now as it’s calculated based on the font size of the Button UIID.

You can even write the code in a more terse style using:

Form hi = new Form("Icon Font");
Button myButton = new Button("Save");
FontImage.setMaterialIcon(myButton, FontImage.MATERIAL_SAVE);
hi.add(myButton);

This will produce the same result for slightly shorter syntax.

FontImage can conflict with some complex APIs that expect a "real" image underneath. Some odd issues can often be resolved by using the toImage() or toEncodedImage() methods to convert the scaled FontImage to a loaded image.

16.11.6. Timeline

Timelines allow rudimentary animation and enable GIF importing using the Codename One Designer. Effectively a timeline is a set of images that can be moved rotated, scaled & blended to provide interesting animation effects. It can be created manually using the Timeline class.

16.11.7. Image masking

Image masking allows you to manipulate images by changing the opacity of an image according to a mask image. The mask image can be hardcoded or generated dynamically, it’s then converted to a Mask object that can be applied to any image. Notice that the masking process is computationally intensive, it should be done once and cached/saved.

The code below can convert an image to a rounded image:

Toolbar.setGlobalToolbar(true);
Form hi = new Form("Rounder", new BorderLayout());
Label picture = new Label("", "Container");
hi.add(BorderLayout.CENTER, picture);
hi.getUnselectedStyle().setBgColor(0xff0000);
hi.getUnselectedStyle().setBgTransparency(255);
Style s = UIManager.getInstance().getComponentStyle("TitleCommand");
Image camera = FontImage.createMaterial(FontImage.MATERIAL_CAMERA, s);
hi.getToolbar().addCommandToRightBar("", camera, (ev) -> {
    try {
        int width = Display.getInstance().getDisplayWidth();
        Image capturedImage = Image.createImage(Capture.capturePhoto(width, -1));
        Image roundMask = Image.createImage(width, capturedImage.getHeight(), 0xff000000);
        Graphics gr = roundMask.getGraphics();
        gr.setColor(0xffffff);
        gr.fillArc(0, 0, width, width, 0, 360);
        Object mask = roundMask.createMask();
        capturedImage = capturedImage.applyMask(mask);
        picture.setIcon(capturedImage);
        hi.revalidate();
    } catch(IOException err) {
        Log.e(err);
    }
});
Picture after the capture was complete and the resulted image was rounded. The background was set to red so the rounding effect will be more noticeable
Figure 224. Picture after the capture was complete and the resulted image was rounded. The background was set to red so the rounding effect will be more noticeable

Notice that this example is simplistic to be self contained. You often recommend that developers ship "ready made" mask images with their application which can allow complex effects on the images.

16.11.8. URLImage

URLImage is an image created with a URL, it implicitly downloads and adapts the image in the given URL while caching it locally. The typical adapt process scales the image or crops it to fit into the same size which is a hard restriction because of the way URLImage is implemented.

How Does URLImage Work?

The reason for the size restriction lies in the implementation of URLImage. URLImage is physically an animated image and so the UI thread tries to invoke its animate() method to refresh. The URLImage uses that call to check if the image was fetched and if not fetches it asynchronously.

Once the image was fetched the animate() method returns true to refresh the UI. During the loading process the placeholder is shown, the reason for the restriction in size is that image animations can’t "grow" the image. They’re assumed to be fixed so the placeholder must match the dimensions of the resulting image.

The simple use case is pretty trivial:

Image i = URLImage.createToStorage(placeholder, "fileNameInStorage", "https://xxx/myurl.jpg", URLImage.RESIZE_SCALE);

Or you can use the similar URLImage.createToFileSystem method instead of the Storage version.

This image can now be used anywhere a regular image will appear, it will initially show the placeholder image and then seamlessly replace it with the file after it was downloaded and stored. Notice that if you make changes to the image itself (for example: the scaled method) it will generate a new image which won’t be able to fetch the actual image.

Since ImageIO is used to perform the operations of the adapter interface its required that ImageIO will work. it’s working in Java SE, Android, iOS & Windows Phone.

If the file in the URL contains an image that’s too big it will scale it to match the size of the placeholder precisely!
An option also exists to fail if the sizes don’t match. Notice that the image that will be saved is the scaled image, this means you will have little overhead in downloading images that are the wrong size although you will get some artifacts.

The last argument is powerful, its an interface called URLImage.ImageAdapter and you can implement it to adapt the downloaded image in any way you like. For example: you can use an image mask to automatically create a rounded version of the downloaded image.

To do this you can override:

public EncodedImage adaptImage(EncodedImage downloadedImage, EncodedImage placeholderImage);

In the adapter interface and return the processed encoded image. If you do heavy processing (for example: rounded edge images) you would need to convert the processed image back to an encoded image so it can be saved. You would then also want to show that this operation should run asynchronously through the appropriate method in the class.

If you need to download the file instantly and not wait for the image to appear before download initiates you can explicitly invoke the fetch() method which will asynchronously fetch the image from the network. Notice that the downloading will still take time so the placeholder is still required.

Authenticated image URLs — RequestDecorator

The standard createToStorage path doesn’t expose the underlying ConnectionRequest, so attaching an Authorization header (or any other custom header / cookie / timeout) requires the RequestDecorator hook. Two ways to install one:

// Global default for createToStorage images from this point on.
// createToFileSystem downloads do not consult it.
// The most common case is "all our images sit behind the same bearer
// token", which has its own shorthand:
URLImage.setDefaultBearerToken(Preferences.get("auth.token", null));

// Or the explicit form, which can attach any header / cookie / timeout:
URLImage.setDefaultRequestDecorator(req ->
        req.addRequestHeader("Authorization", "Bearer " + token));

For per-image overrides (for example one endpoint needs an extra API-version header on top of the global bearer token), use the createToStorage(placeholder, key, url, adapter, RequestDecorator) overload. Per-instance decorators run after the global default so they can override or augment whatever the default set:

URLImage profilePic = URLImage.createToStorage(
        placeholder,
        "profile-" + userId,
        baseUrl + "/users/" + userId + "/picture",
        URLImage.RESIZE_SCALE_TO_FILL,
        req -> req.addRequestHeader("X-API-Version", "2"));
Decorators reach the storage-backed images only. URLImage.fetch() downloads a file-system-backed image through Util.downloadImageToFileSystem, which takes neither the global default nor a per-image decorator, so an image created with createToFileSystem is fetched without your Authorization header. Use createToStorage when the endpoint needs one.

When a decorator is installed (global or per-call), URLImage skips the default Util.downloadImageToStorage path and builds a ConnectionRequest inline so the decorator can inspect / mutate it before it’s queued. The legacy path remains the default for back-compat when no decorator is set.

Mask adapter

A URLImage can be created with a mask adapter to apply an effect to an image. This allows you to round downloaded images or apply any sort of masking for example: you can adapt the round mask code above as such:

Image roundMask = Image.createImage(placeholder.getWidth(), placeholder.getHeight(), 0xff000000);
Graphics gr = roundMask.getGraphics();
gr.setColor(0xffffff);
gr.fillArc(0, 0, placeholder.getWidth(), placeholder.getHeight(), 0, 360);

URLImage.ImageAdapter ada = URLImage.createMaskAdapter(roundMask);
Image i = URLImage.createToStorage(placeholder, "fileNameInStorage", "https://xxx/myurl.jpg", ada);
The adapter masks the image while resizing it to the placeholder, so a download that already matches the placeholder’s dimensions is stored unmasked. Give the placeholder a size the source images don’t already have when every image has to come out rounded.
URLImage in lists

The biggest problem with image download service is with lists. Codename One attacks this issue at the core by integrating URLImage support directly into GenericListCellRenderer which means it will work with MultiList, List & ContainerList. To use this support define the name of the component (name not UIID) to end with _URLImage and give it an icon to use as the placeholder. This is easy to do in the multilist by changing the name of icon to icon_URLImage then using this in the data:

map.put("icon_URLImage", urlToActualImage);

Make sure you also set a "real" icon to the entry in the GUI builder or in handcoded applications. This is important since the icon will be implicitly extracted and used as the placeholder value. Everything else should be handled automatically. You can use setDefaultAdapter & setAdapter on the generic list cell renderer to install adapters for the images. The default is a scale adapter although you might change that to scale fill in the future:

Style s = UIManager.getInstance().getComponentStyle("Button");
FontImage p = FontImage.createMaterial(FontImage.MATERIAL_PORTRAIT, s);
EncodedImage placeholder = EncodedImage.createFromImage(p.scaled(p.getWidth() * 3, p.getHeight() * 4), false);

Form hi = new Form("MultiList", new BorderLayout());

ArrayList<Map<String, Object>> data = new ArrayList<>();

data.add(createListEntry("A Game of Thrones", "1996", "https://www.georgerrmartin.com/wp-content/uploads/2013/03/GOTMTI2.jpg"));
data.add(createListEntry("A Clash Of Kings", "1998", "https://www.georgerrmartin.com/wp-content/uploads/2012/08/clashofkings.jpg"));
data.add(createListEntry("A Storm Of Swords", "2000", "https://www.georgerrmartin.com/wp-content/uploads/2013/03/stormswordsMTI.jpg"));
data.add(createListEntry("A Feast For Crows", "2005", "https://www.georgerrmartin.com/wp-content/uploads/2012/08/feastforcrows.jpg"));
data.add(createListEntry("A Dance With Dragons", "2011", "https://georgerrmartin.com/gallery/art/dragons05.jpg"));
data.add(createListEntry("The Winds of Winter", "2016 (please, please, please)", "https://www.georgerrmartin.com/wp-content/uploads/2013/03/GOTMTI2.jpg"));
data.add(createListEntry("A Dream of Spring", "Ugh", "https://www.georgerrmartin.com/wp-content/uploads/2013/03/GOTMTI2.jpg"));

DefaultListModel<Map<String, Object>> model = new DefaultListModel<>(data);
MultiList ml = new MultiList(model);
ml.getUnselectedButton().setIconName("icon_URLImage");
ml.getSelectedButton().setIconName("icon_URLImage");
ml.getUnselectedButton().setIcon(placeholder);
ml.getSelectedButton().setIcon(placeholder);
hi.add(BorderLayout.CENTER, ml);
hi.show();

The createListEntry method then looks like this:

private Map<String, Object> createListEntry(String name, String date, String coverURL) {
    Map<String, Object> entry = new HashMap<>();
    entry.put("Line1", name);
    entry.put("Line2", date);
    entry.put("icon_URLImage", coverURL);
    entry.put("icon_URLImageName", name);
    return entry;
}
A URL image fetched dynamically into the list model
Figure 225. A URL image fetched dynamically into the list model

16.12. Charts

Codename One includes a charting toolkit in the com.codename1.charts package that’s designed to integrate with regular UI layouts. Charts are drawn by creating an appropriate dataset and renderer pair, instantiating the matching chart view class, and wrapping it in a ChartComponent it can be added to a form:

XYSeriesRenderer seriesRenderer = new XYSeriesRenderer();
seriesRenderer.setColor(0xff0000);

XYMultipleSeriesRenderer renderer = new XYMultipleSeriesRenderer();
renderer.addSeriesRenderer(seriesRenderer);

XYSeries series = new XYSeries("Sales");
series.add(1, 42);
series.add(2, 57);

XYMultipleSeriesDataset dataset = new XYMultipleSeriesDataset();
dataset.addSeries(series);

BarChart chart = new BarChart(dataset, renderer, BarChart.Type.DEFAULT);
Form form = new Form(new BorderLayout());
form.add(BorderLayout.CENTER, new ChartComponent(chart));
form.show();

The following classes are available for different kinds of visualisations:

Chart classDataset & rendererNotes

BarChart

XYMultipleSeriesDataset / XYMultipleSeriesRenderer

Draws categorical data as vertical bars. The Type constructor parameter controls default, stacked, or heaped bars.

BubbleChart

XYMultipleSeriesDataset containing XYValueSeries / XYMultipleSeriesRenderer

Represents each data point as a circle whose size is proportional to a third value.

CombinedXYChart

XYMultipleSeriesDataset / XYMultipleSeriesRenderer

Combines several XY chart types in a single plot using XYCombinedChartDef to map series to chart implementations.

CubicLineChart

XYMultipleSeriesDataset / XYMultipleSeriesRenderer

Smooths line series with cubic interpolation. Pass a smoothness factor to the constructor to control the curve.

DialChart

CategorySeries / DialRenderer

Renders one or more gauges on a dial, making it useful for KPI dashboards.

DoughnutChart

MultipleCategorySeries / DefaultRenderer

Shows hierarchical proportions as concentric rings around a common center.

LineChart

XYMultipleSeriesDataset / XYMultipleSeriesRenderer

Connects series of points using straight line segments. Supports optional fill areas and point markers.

PieChart

CategorySeries / DefaultRenderer

Splits a circle into slices that are proportional to each category value.

RadarChart

AreaSeries / DefaultRenderer

Draws a spider/web chart that compares multiple categories across the same set of axes.

RangeBarChart

XYMultipleSeriesDataset / XYMultipleSeriesRenderer

Variation of BarChart that uses paired min/max values to render ranges.

RoundChart

CategorySeries / DefaultRenderer

Abstract base class for circular charts. Use subclasses such as PieChart, DoughnutChart, or DialChart directly.

ScatterChart

XYMultipleSeriesDataset / XYMultipleSeriesRenderer

Plots unconnected X/Y points with configurable marker shapes.

TimeChart

XYMultipleSeriesDataset / XYMultipleSeriesRenderer

Extends LineChart with date-aware labelling on the X axis for time series data.

17. Build-Time Vector & Animation Images

The build-time vector transcoder lets you author UI icons and illustrations as SVG or Lottie / Bodymovin JSON and have them rendered as native Codename One Image instances on every platform (iOS, Android, JavaSE simulator, JavaScript) without shipping a runtime SVG or Lottie parser.

Both formats share the same pipeline: each source file is lowered into the SVG transcoder’s model, the same JavaCodeGenerator emits a com.codename1.ui.GeneratedSVGImage subclass, and the same SVGRegistry makes the result available via Resources.getImage("name.<ext>"). The rest of this guide therefore covers SVG in detail; Lottie gets its own section at the end that only calls out the parts that differ.

17.1. Motivation

Codename One’s older Image.createSVG() API depends on a per-platform native SVG renderer that ships only on the JavaSE simulator; iOS and Android applications fall back to PNG multi-image buckets, giving up the resolution-independence that SVG was designed for. The legacy Flamingo SVG transcoder was a precursor to this work — a build-time tool that emitted Java drawing code from an SVG — but it pre-dated CN1’s affine transform pipeline and was missing a Maven hook, CSS integration, animations, and most of the SVG spec.

This transcoder is its successor and covers each of those gaps. Every SVG you drop into src/main/css/ is parsed by codenameone-svg-transcoder, emitted as a Java class that subclasses com.codename1.ui.GeneratedSVGImage, and rendered through the standard Graphics shape API. The same vector source produces pixel-perfect output at any size on every port.

17.2. Quick start

  1. Drop your SVG next to theme.css in src/main/css/:

    src/main/css/
      theme.css
      home.svg
  2. Reference it from CSS and pin its rendered size in millimeters — the recommended sizing knob, since it carries unchanged across every device DPI:

    HomeIcon {
        background: url(home.svg);
        cn1-svg-width: 6mm;
        cn1-svg-height: 6mm;
        cn1-background-type: cn1-image-scaled-fit;
    }
  3. Build the project:

    mvn package

That’s it. App code doesn’t call into the registry. The transcoder emits com.codename1.generated.svg.SVGRegistry only when SVGs are present, and the per-port build wiring (IPhoneBuilder, AndroidGradleBuilder, the JavaSE port) loads it automatically before init(Object). Resources.getGlobalResources().getImage("home.svg") returns the transcoded image; CSS rules that reference the SVG by URL pick it up via the same registry.

17.3. Why millimeters

cn1-svg-width / cn1-svg-height are the sizing knob you should reach for first.

  • A 6mm icon is 6mm tall on a 1x desktop, 6mm on a high-DPI handset, and 6mm on a 4K tablet. The transcoder routes both values through Display.convertToPixels() at install time, the same way font-size: 3mm behaves elsewhere in CN1 CSS.

  • SVGs in the wild routinely declare odd width/height attributes (a 1024×1024 export of a 24×24 icon, no dimensions at all, etc.). Pinning the rendered size in millimeters sidesteps that guesswork — the SVG’s declared pixel size and the device DPI become irrelevant.

  • It’s the only sizing mode that survives a re-export of the SVG from the asset pipeline. cn1-source-dpi and the implicit medium-density default both depend on the SVG’s declared pixel size, so a tool that rescales the source on export will change the rendered size without any warning.

StarIcon { background: url(star.svg); cn1-svg-width: 4mm; cn1-svg-height: 4mm; }
PrimaryIcon { background: url(home.svg); cn1-svg-width: 6mm; cn1-svg-height: 6mm; }
LogoBanner { background: url(logo.svg); cn1-svg-width: 32mm; cn1-svg-height: 12mm; }

Use cn1-source-dpi: <bucket> (accepted keywords: low, medium, high, very-high, hd, 560, 2hd, 4k) only when an SVG has sensible declared pixel dimensions for a known density target and you want the rendered size to track Display.getDeviceDensity() instead of millimeters. With no hint at all, the SVG’s declared dimensions are treated as design pixels at DENSITY_MEDIUM.

If both keys appear on the same rule, cn1-svg-width / cn1-svg-height wins.

17.4. How the build flow works

The cn1app archetype binds the transcode-svg goal to the generate-sources phase:

<execution>
    <id>transcode-svg</id>
    <phase>generate-sources</phase>
    <goals>
        <goal>transcode-svg</goal>
    </goals>
</execution>

That execution was added to the archetype after the Maven project format itself shipped, so a project generated before it has no transcoder bound. Because nothing fails when the transcoder is missing — the CSS compiler leaves a transparent placeholder in the theme and the app renders nothing where the icon should be — the build repairs the gap rather than waiting to be asked. A module that contains vector assets but binds no transcode-svg execution is transcoded in place during the current build and has the execution written into its POM, with the previous contents kept as pom.xml.bak. A project that already binds the goal, or that has no vector assets, is left untouched.

When Maven runs the mojo:

  1. It scans src/main/css/ and src/main/svg/ for *.svg files. If there are none, the goal is a no-op — no registry class is emitted, no per-port stub injection happens.

  2. For each SVG it emits target/generated-sources/svg/com/codename1/generated/svg/<Name>.java — one class per file — plus a single SVGRegistry class whose installGlobal() registers every transcoded image with the global Resources table.

  3. It scans src/main/css/*/.css for url(*.svg) occurrences and the enclosing rule’s cn1-svg-width / cn1-svg-height / cn1-source-dpi hints. Those values are baked into the registry’s installGlobal() call per image.

  4. It drops a 1×1 transparent PNG placeholder into target/css-resources/ so the standalone CSS compiler can finish even though the file extension on the URL is .svg. The placeholder is overwritten in the theme by the runtime registry call.

The simulator / desktop ports load SVGRegistry reflectively, so a project that adds its first SVG today rebuilds and runs without any code change. The iOS and Android builders detect the generated class in the user’s compile output and weave installGlobal() into the generated Stub right before the first init(Object); a project with no SVGs gets no weaving.

17.5. Calling the registry yourself

If you don’t use theme.css but still want a transcoded SVG, construct the generated class directly. It lands in com.codename1.generated.svg and is named after the file, so src/main/svg/logo.svg becomes com.codename1.generated.svg.Logo. The class exists only after a build has run the transcoder, which is why this listing isn’t one of the compiled examples:

import com.codename1.generated.svg.Logo;
import com.codename1.ui.Display;

Logo logo = new Logo();                      // declared size at DENSITY_MEDIUM
Logo dense = new Logo(Display.DENSITY_HIGH); // read as a higher-density design
Logo sized = new Logo(12f, 12f);             // 12mm square, converted per device

myButton.setIcon(sized);

Constructors come in three flavours, matching the three CSS sizing mechanisms above. The two-float constructor takes millimeters; that’s the one to prefer for the reasons in the previous section.

scaled(int, int) returns a lightweight view that reports the requested dimensions from getWidth() / getHeight() and shares the animation clock with its source.

17.6. SVG feature coverage

The transcoder targets the SVG 1.1 static-shape vocabulary plus the SMIL animation subset:

FeatureStatus

<rect>, <circle>, <ellipse>, <line>, <polyline>, <polygon>

Full

<path> (M/L/H/V/C/S/Q/T/A/Z, relative + smooth-curve reflection)

Full

<g> with transform="translate | scale | rotate | skew | matrix"

Full

fill, stroke, stroke-width, stroke-linecap/linejoin/miterlimit

Full

opacity, fill-opacity, stroke-opacity

Full (animatable)

<linearGradient>

Full (shape-clipped fill on every port)

<radialGradient>

First-stop fallback

<animate> (numeric attributes)

Full

<animateTransform type="translate | scale | rotate">

Full

<set> (instant value snap)

Full

<text> / <tspan> (single-style fills, transforms)

Supported

<clipPath> referenced via clip-path="url(#id)"

Supported (rect/circle/path); nested clip refs ignored

<mask>

Treated as clip — alpha masking falls back to opaque

SVG filter primitives

Not supported

CSS-keyframe animations

Not supported (SMIL only)

SMIL animations read the current time from com.codename1.ui.animations.AnimationTime, so tests can pin the clock with AnimationTime.setTime(t) to capture a deterministic frame. Animated SVGs return true from Image.isAnimation() — as with Timeline, you must register the image with a Form’s animation manager (or set it as a `Component.setIcon with isAnimation() true) for the repaint loop to tick the SMIL clock.

17.7. Lottie animations

Lottie / Bodymovin JSON files are picked up by the same transcode-svg goal. Drop them next to your CSS (or under src/main/lottie/) and the Lottie parser lowers each one into the SVG model — so everything in the previous sections (sizing keys, registry, theme url(…​) lookup, Resources.getImage(…​), the per-port wiring) applies unchanged:

src/main/css/
  theme.css
  spinner.json     <-- Lottie/Bodymovin export
SpinnerStyle {
    background: url(spinner.json);
    cn1-svg-width: 12mm;
    cn1-svg-height: 12mm;
    cn1-background-type: cn1-image-scaled-fit;
}
Image spin = Resources.getGlobalResources().getImage("spinner.json");
// or by stem, like a multi-image:
Image spin2 = Resources.getGlobalResources().getImage("spinner");

.lottie (dotLottie ZIP) files are accepted alongside .json for forward compatibility, but the archive container isn’t yet extracted — export your animation as a plain Bodymovin JSON for now.

17.7.1. Lottie feature coverage

The parser targets the subset of Bodymovin a "spinner" or "pulse" animation typically uses. Anything outside the subset is dropped without warning so a file with mixed coverage still produces a renderable class:

FeatureStatus

Shape layers (ty:4) with grouped rc / el / sh primitives

Full

Solid color layers (ty:1)

Rendered as a filled rect

Shape fills (fl) and strokes (st) — solid colors

Full

Layer transform (anchor, position, scale, rotation, opacity) — static

Full

Animated rotation / position / scale — 2 keyframes

Full (loops indefinitely over the comp duration)

Animated colors / opacity

Collapsed to the first keyframe

Bezier easing on keyframes

Linear interpolation (easing curves ignored)

Multi-keyframe properties (3+)

Collapsed to first vs. last (matches the SVG codegen’s from/to model)

Trim path (tm), repeater (rp), merge (mm), rounded corners (rd)

Ignored

Gradient fills (gf) / gradient strokes (gs)

Ignored

Text layers, image layers, precomp, mattes, expressions

Ignored

For animations that need higher fidelity than the subset above, export the relevant frame as an SVG and use the SVG transcoder path directly — the runtime classes are identical.

17.8. Troubleshooting

Resources.getImage("name.svg") returns null or a 1×1 transparent PNG

The transcoder ran with zero source files: the name in the CSS url(…​) doesn’t match any file under src/main/css/ (or src/main/svg/ / src/main/lottie/ for the dedicated directories). The build names each unmatched reference in a warning. A POM that doesn’t bind the transcode-svg goal at all is repaired automatically, so it’s not the likely cause. The same goal handles .svg, .json, and .lottie — one goal, both formats. For arbitrary Resources bundles loaded outside the global slot, call com.codename1.ui.util.Resources.registerGeneratedImage(name, image) yourself with a fresh new com.codename1.generated.svg.YourAsset(widthMm, heightMm).

Lottie animation looks frozen or starts halfway through

The Lottie parser collapses each animated property’s keyframe array to its first and last value, then loops indefinitely over the composition’s duration. Animations with three or more keyframes on the same property therefore play as a straight first-to-last interpolation. Re-export the comp split into two-keyframe segments or use an SVG/SMIL export for that animation if you need exact keyframe playback.

SVG looks the wrong size

Switch the rule to cn1-svg-width / cn1-svg-height in millimeters. The other sizing modes depend on the SVG’s declared pixel size, which is what’s biting you here.

Spinner / pulse renders but doesn’t animate

Check that the image is mounted into a Component whose animation manager ticks (Labels added to a Form do this automatically). For a pure-Graphics-driven capture (screenshot tests) pin AnimationTime.setTime(…​) and re-render.

18. 3D Graphics and Shaders

The com.codename1.gpu package provides a portable, hardware accelerated 3D graphics API focused on games but useful anywhere you need GPU rendered content. It runs from one code base on the JavaSE simulator (OpenGL via JOGL), Android (OpenGL ES 2), iOS and macOS (Metal), the native Windows port (Direct3D 11), and the JavaScript port (WebGL). The API integrates with the normal Codename One UI: a 3D scene lives inside a regular component that you add to a Form like any other.

Your Renderer callbacks run on the native platform render thread — NOT the Codename One EDT. This is the single most important thing to understand before you write any 3D code.

onInit, onResize, onFrame and onDispose are invoked on the underlying OS/GPU render loop (GL / Metal / Direct3D), not on the event dispatch thread. From inside them you must not touch Codename One UI components, show forms, or call other EDT-only APIs. To push data back to the UI (for example to update a HUD label or react to a game event) hop onto the EDT with CN.callSerially(…​). Conversely, treat the GraphicsDevice and the objects it hands you as owned by the render thread.

3D is a low-level, GPU dependent feature. Always guard usage with CN.isGpuSupported() (or RenderView.isSupported()); on a platform without a backend the RenderView reports unsupported so you can fall back to 2D.

18.1. Concepts

The API is intentionally "hybrid": a low level command layer (buffers, textures, render state, draw calls) for full control, plus high level helpers (meshes, materials, a camera) for common cases. Crucially, you never write shader source. Instead you describe a Material (a lighting model plus color and texture) and the engine generates the matching platform shader behind the scenes: GLSL ES on OpenGL ES and WebGL, GLSL on the desktop simulator, Metal Shading Language on iOS and Mac, and HLSL on the native Windows (Direct3D) port. This "engine-managed shader" approach is what keeps the same application code rendering identically across a range of different GPUs.

The main types are:

  • RenderView - the Component that hosts the GPU surface. You give it a Renderer.

  • Renderer - your callback: onInit, onResize, onFrame, onDispose. These run on the platform render thread, never the EDT.

  • GraphicsDevice - the command surface passed to your renderer. It creates buffers and textures, clears, sets the viewport, the camera and the light, and issues draw calls.

  • Mesh, Material, Camera, Light - the high level scene building blocks.

18.2. A first scene: A spinning cube

The renderer below draws a Phong lit cube. Primitives.cube builds the geometry, a Material describes the surface, and a Camera supplies the view. Notice there is no shader code anywhere.

setContinuous(true) runs an animation loop. For a static scene leave it off and call view.requestRender() whenever something changes; this conserves battery.

A Phong lit cube rendered by RenderView
Figure 226. A Phong lit cube rendered by RenderView in the simulator

18.3. Materials

A Material is a declarative description of a surface. Its Type selects the lighting model:

TypeDescription

UNLIT

Flat color/texture, no lighting. Ideal for UI, emissive surfaces.

LAMBERT

Diffuse (Lambert) lighting from one directional light.

PHONG

Diffuse + specular highlight (uses setShininess).

SPRITE

Unlit, for screen aligned sprites and billboards.

SKYBOX

Unlit background, rendered behind the scene.

A material also carries a base color (setColor, packed 0xAARRGGBB), an optional Texture (setTexture), and a RenderState controlling depth testing, alpha blending and face culling. Textures come from a Codename One Image or raw ARGB pixels:

Texture tex = device.createTexture(myImage);     // or createTexture(w, h, argb)
// NEAREST keeps texel edges crisp, which is what the checkerboard in the
// figure below needs. LINEAR is the better default for photographic
// textures, where smoothing is wanted rather than avoided.
tex.setFilter(Texture.Filter.NEAREST).setWrap(Texture.Wrap.REPEAT);
Material m = new Material(Material.Type.UNLIT).setTexture(tex);
A checkerboard textured cube
Figure 227. An UNLIT cube with a checkerboard texture (NEAREST filtering)

18.4. Meshes and buffers

Primitives builds common shapes (cube, quad). For custom geometry, allocate a VertexBuffer with a VertexFormat, fill the interleaved float data, and (optionally) an IndexBuffer:

VertexBuffer vb = device.createVertexBuffer(VertexFormat.POSITION_NORMAL_TEXCOORD, 4);
vb.setData(new float[] { /* px,py,pz, nx,ny,nz, u,v  per vertex null */ });
IndexBuffer ib = device.createIndexBuffer(6);
ib.setData(new int[] { 0, 1, 2, 0, 2, 3 });
Mesh mesh = new Mesh(vb, ib, PrimitiveType.TRIANGLES);

Vertex buffers are allocated through the platform SIMD allocator, which on iOS (ParparVM) places the data at a fixed, aligned native address so it can be handed to Metal with no intermediate copy. You don’t need to do anything special to get this; just write into getData() and call setDirty() when you mutate it.

18.5. Loading models

Real scenes use authored geometry. GltfLoader loads a glTF 2.0 model, both the binary .glb container and the JSON .gltf form with embedded buffers. loadModel returns the mesh together with the base-color texture from the model’s own material, so a textured model renders with no extra setup:

InputStream in = Display.getInstance().getResourceAsStream(getClass(), "/boombox.glb");
GltfLoader.GltfModel loaded = GltfLoader.loadModel(device, in);
Material material = new Material(Material.Type.PHONG).setShininess(16f);
if (loaded.getBaseColorTexture() != null) {
    material.setTexture(loaded.getBaseColorTexture());
}
Mesh model = loaded.getMesh();           // draw it like any other mesh

The model below is the Khronos "BoomBox" glTF sample (a CC0 model carrying its own base-color texture), loaded this way and drawn Phong lit:

A textured glTF model loaded with GltfLoader
Figure 228. The Khronos BoomBox glTF sample loaded with GltfLoader

18.6. Animation

Drive a value (a rotation angle, a position) over time and redraw. A continuous RenderView calls onFrame every frame; an on-demand view redraws when you call requestRender(). The grid below captures six fixed rotation stages of a spinning cube, each drawn into its own viewport in a single frame:

Six rotation stages of an animated cube
Figure 229. Six rotation stages of an animated cube captured in one frame

18.7. Camera and math

Camera builds the view and projection matrices from an eye position, a look-at target and lens settings (setPerspective or setOrthographic). All matrix helpers live in Matrix4 (column-major float[16]): translation, scaling, rotation, multiply, lookAt, perspective, ortho. Pass a model matrix as the third argument to draw, or null for the identity.

18.8. Platform notes

  • JavaSE simulator - OpenGL through JOGL, loaded from an isolated class so a missing or failing GL driver degrades to a built in software rasterizer instead of breaking the simulator. Lets you develop and debug 3D without a device.

  • Android - OpenGL ES 2 via a GLSurfaceView hosted as a native peer.

  • iOS and macOS - Metal. Shaders are generated as Metal Shading Language and compiled at runtime; vertex/index data is uploaded to `MTLBuffer`s, zero-copy where the SIMD allocation permits.

  • Native Windows - Direct3D 11. Shaders are generated as HLSL and compiled with D3DCompile; the offscreen render target is read back and composited into the UI.

  • JavaScript - WebGL on a <canvas> peer; the generated GLSL ES runs unmodified.

Querying capabilities at runtime:

if (CN.isGpuSupported()) {
    GpuCapabilities caps = device.getCapabilities();
    Log.p("GPU " + caps.getRendererName()
            + ", max texture " + caps.getMaxTextureSize()
            + ", shader level 3: " + caps.isShaderLevel3());

    // Size the atlas to what this device can actually hold rather than
    // assuming a figure: the limit is as low as 2048 on older hardware.
    int edge = Math.min(2048, caps.getMaxTextureSize());
    Log.p("atlas edge " + edge);

    if (caps.isIntIndicesSupported()) {
        // 32-bit indices are available, so a mesh may exceed 65535 vertices
    }
}

18.9. Threading

Renderer callbacks run on the platform render thread, not the Codename One EDT. Don’t touch UI components from inside them. To move data the other way (for example to update a HUD label from a game loop), use CN.callSerially(…​).

19. Augmented Reality

The com.codename1.ar package composites virtual 3D content into the live camera image: the session tracks the device’s position in the real world, detects surfaces, recognizes registered images and faces, and keeps virtual objects locked to real-world positions as the user moves. It’s backed by ARKit on iOS and by ARCore (Google Play Services for AR) on Android, behind one portable API.

Referencing any class in the package is enough to wire the build: the class scanner injects the camera permission, the NSCameraUsageDescription plist entry (override the text with the ios.NSCameraUsageDescription build hint), links ARKit and SceneKit on iOS and adds the ARCore dependency plus the com.google.ar.core manifest entry on Android. Apps that never touch the package pay no size, permission or store-visibility cost. By default the Android manifest marks AR optional so the app still installs on devices without ARCore; set the android.ar.required=true build hint to make the app AR-only.

Always guard AR functionality behind AR.isSupported(). Older devices, tvOS, watchOS, desktop builds and browsers report false; feature-specific hardware such as face tracking is gated through AR.getCapabilities().

19.1. Concepts

TypeRole

AR

Static entry point: support and capability checks, permission request, session opening.

ARSession

The running AR experience. One session may be open at a time; it owns the view, the anchors and the detected planes, and delivers all events.

ARView

The component that renders the camera image composited with the anchored content. Add it to a form like any other component.

ARPose

An immutable position + rotation in world space: meters, right-handed, Y up, -Z forward from the initial camera direction. Converts to a com.codename1.gpu.Matrix4 compatible matrix.

ARPlane

A detected real-world surface (floor, table, wall) that grows and refines as the session learns the environment.

ARHitResult

The intersection of a screen-point ray with real-world geometry; the bridge from a tap to a world position.

ARAnchor

A tracked fixed point in the real world. The attachment point for content; also delivered for recognized images (ARImageAnchor) and faces (ARFaceAnchor).

ARNode / ARModel

The content placed at an anchor: a small scene graph of nodes carrying glTF models or com.codename1.gpu.Mesh geometry.

ARLightEstimate

The real-world lighting estimate, polled per frame to shade content so it blends in.

19.2. Getting started: Place a model on the floor

The canonical AR loop is: open a session, show its view, wait for a plane, hit test a tap, anchor content at the hit.

if (!AR.isSupported()) {
    ToastBar.showInfoMessage("AR is not supported on this device");
    return;
}
final ARSession session = AR.open(new ARSessionOptions());
final ARView view = session.createView();

Form f = new Form("AR", new BorderLayout());
f.add(BorderLayout.CENTER, view);
f.show();

// modelBytes is a .glb asset, e.g. loaded from getResourceAsStream
view.addPointerReleasedListener(e -> {
    float xn = (e.getX() - view.getAbsoluteX()) / (float) view.getWidth();
    float yn = (e.getY() - view.getAbsoluteY()) / (float) view.getHeight();
    session.hitTest(xn, yn).ready(hits -> {
        if (hits.length > 0) {
            ARAnchor anchor = hits[0].createAnchor();
            anchor.setNode(new ARNode(ARModel.fromGltf(modelBytes)));
        }
    });
});

ARSessionOptions configures the session before it opens: the tracking mode (WORLD or FACE), which plane orientations to detect, whether to estimate lighting, and the reference images to recognize. The defaults open a world tracking session with horizontal plane detection and light estimation, which suits the place-content-on-a-surface use case above.

19.3. Plane detection

Planes are delivered through ARPlaneListener as immutable snapshots: an ADDED event when a new surface is found, UPDATED snapshots (sharing the same getId()) as the surface grows, and REMOVED when a plane merges into another. ARSession.getPlanes() always reflects the latest snapshots.

session.addPlaneListener(ev -> {
    ARPlane p = ev.getPlane();
    System.out.println(ev.getKind() + " " + p.getType()
            + " " + p.getExtentX() + "x" + p.getExtentZ() + "m");
});

The plane’s local frame has X/Z spanning the surface and Y along the normal; getCenterPose() positions that frame in the world. ARPlane.Type distinguishes upward-facing horizontal surfaces (floors, tables), downward-facing ones (ceilings) and vertical surfaces (walls) - request vertical detection with ARSessionOptions.planeDetection(ARPlaneDetection.HORIZONTAL_AND_VERTICAL).

19.4. Hit testing and anchors

hitTest(xNorm, yNorm) shoots a ray from a normalized view coordinate into the world and resolves - on the EDT - with the intersections ordered nearest first. Prefer ARHitResult.createAnchor() over session.createAnchor(pose) when placing on detected geometry: the platform can then anchor to the exact native raycast, which tracks better as the environment refines.

Anchors survive tracking refinements: the session updates getPose() over time and reports the change through ARAnchorListener. Remove an anchor (and its content) with detach(). Content attaches through setNode(ARNode):

ARNode root = new ARNode(ARModel.fromMesh(
        Primitives.sphere(0.15f, 24, 32, false), 0xffdd4433));
root.setLocalPosition(0, 0.15f, 0); // rest on the surface, not in it
anchor.setNode(root);

Node transforms are relative to the anchor, in meters. Nodes may nest (addChild), carry glTF assets (ARModel.fromGltf) or portable meshes (ARModel.fromMesh), and any mutation after attachment - position, rotation, scale, visibility, children - re-syncs the platform renderer automatically.

19.5. Light estimation

session.getLightEstimate() returns the latest estimate of the real-world lighting. It refreshes every frame without firing events, so poll it when rendering-relevant decisions are made. The ambient intensity is normalized so 1.0 means neutral indoor lighting; the color correction is a per-channel scale where {1, 1, 1} is neutral. Platform renderers already apply the estimate to anchored content; the API exposes it for custom logic such as prompting the user when the room is too dark.

19.6. Image tracking

Register printed images - posters, game boards, product labels - and the session anchors content to them when the camera sees them:

ARSessionOptions opts = new ARSessionOptions().referenceImages(new ARReferenceImage[]{
    new ARReferenceImage("poster", posterPngBytes, 0.42f) // physical width in meters
});
ARSession session = AR.open(opts);
session.addAnchorListener(ev -> {
    if (ev.getKind() == ARAnchorEvent.Kind.ADDED
            && ev.getAnchor() instanceof ARImageAnchor) {
        ARImageAnchor img = (ARImageAnchor) ev.getAnchor();
        System.out.println("Recognized " + img.getReferenceImageName());
        img.setNode(new ARNode(ARModel.fromGltf(overlayModel)));
    }
});

The physical width matters: the platform uses it to estimate the image’s distance, so measure the real print. The anchor pose is centered on the physical image with local X/Z spanning its surface.

19.7. Face tracking

Face tracking is a distinct session configuration using the front camera; open it with ARSessionOptions.trackingMode(ARTrackingMode.FACE) after checking AR.getCapabilities().isFaceTrackingSupported(). Detected faces arrive as `ARFaceAnchor`s through the anchor listener, carrying the head pose plus optional finer geometry:

  • getRegionPose(ARFaceRegion) returns the pose of a named region. Availability differs per platform (ARCore supplies the nose tip and forehead regions, ARKit supplies the eyes), which is why code must always handle a null region.

  • getMeshVertices() / getMeshTriangles() expose the tracked face mesh where the platform provides one, or null where it doesn’t.

19.8. Events and threading

Every listener fires on the EDT and every getter reflects the state as of the events delivered so far, so AR code follows normal Codename One threading rules with no extra synchronization. High-frequency refinements (anchor and plane updates, camera pose, light estimate) are coalesced: a slow EDT sees the latest values rather than a backlog. This contrasts with the com.codename1.gpu.Renderer callbacks used by the VR API, which run on the render thread.

19.9. The simulator and debugging

The Codename One simulator ships a simulated AR backend modeled on the Android emulator’s virtual scene, so the full AR loop is debuggable with breakpoints without a device. When a session opens, the simulator "detects" the floor (and, when requested, a wall) of a virtual room after a short startup delay, mimicking real AR initialization. The AR view renders the room and every anchored model; drive the virtual device camera with the mouse (drag to look around) and keyboard (W/A/S/D or the arrow keys to move, Q/E for up and down).

A model placed on the virtual room floor in the simulator

The Simulate menu’s AR Simulation window drives the paths that are hard to reproduce on demand with real hardware: force degraded tracking states and failure reasons, change the light estimate, re-run plane detection, trigger recognition of a registered reference image, and toggle a simulated face anchor for FACE sessions.

On devices, use the standard native tools: the generated Xcode project debugs the ARKit session and SceneKit scene directly (including Instruments), and Android Studio attaches to the Gradle project for ARCore logging. Note that Apple’s iOS Simulator has no ARKit support - real ARKit behavior needs a physical device - while the Android emulator can run ARCore against its own virtual scene.

19.10. Platform notes

  • Dependencies: injected automatically as described above. The ARCore dependency adds about 2 MB to Android binaries; iOS links the system ARKit/SceneKit frameworks at no size cost.

  • Capabilities: face tracking requires a capable front camera (a TrueDepth camera on iOS). Check ARCapabilities per feature rather than assuming.

  • Face regions: ARCore supplies NOSE_TIP, FOREHEAD_LEFT and FOREHEAD_RIGHT; ARKit supplies LEFT_EYE and RIGHT_EYE. Code that uses regions must handle a null region.

  • ARCore installation: on Android, Play Services for AR may need installing or updating; when AR.open triggers that flow it throws with a message saying to retry once installation completes.

  • tvOS / watchOS: AR compiles out entirely; AR.isSupported() reports false.

20. Virtual Reality and 360 Media

The com.codename1.vr package renders application scenes in stereoscopic VR and displays 360-degree panoramas, built entirely on the portable com.codename1.gpu pipeline and the com.codename1.sensors motion sensors. There is no platform SDK dependency: everything here runs wherever Display.getInstance().isGpuSupported() is true, which includes the simulator, and adds nothing to the build for apps that don’t use it.

Like com.codename1.gpu.Renderer, the VRRenderer and TextureSource callbacks run on the platform render thread that owns the GPU context - never touch Codename One UI components from them. Everything else in the package is ordinary EDT-friendly component API.

20.1. Concepts

TypeRole

VRView

The stereo component: clears the frame, splits the viewport per eye, positions the cameras from head tracking and invokes your renderer once per eye.

VRRenderer

Your scene callback: onInit, onEyeFrame(device, eye, camera) and onDispose.

VREye

Which eye a frame is for: LEFT, RIGHT, or CENTER in mono mode.

VRSettings

Interpupillary distance (default 0.064m), per-eye field of view, clip planes and the stereo default.

HeadTracker

Wires the gyroscope, accelerometer and magnetometer into an orientation with thread-safe snapshots for the render thread.

OrientationFilter

The deterministic sensor-fusion math behind the tracker, usable on its own.

VRCameraRig

Per-eye camera math over the existing com.codename1.gpu.Camera, usable without VRView for custom pipelines.

Media360View

A 360-degree panorama viewer: an equirectangular image on the inside of a sphere with drag or gyroscope look-around.

TextureSource

Extension point feeding dynamic textures (procedural animation, future video) to Media360View.

20.2. Getting started: A stereo scene

A VRRenderer draws the same scene for every eye; the camera differences produce the depth effect. The view configures the viewport and camera before each onEyeFrame, so the renderer only issues draw calls:

VRView vr = new VRView(new VRRenderer() {
    Mesh cube;
    Material material;

    public void onInit(GraphicsDevice device) {
        cube = Primitives.cube(device, 0.5f);
        material = new Material(Material.Type.PHONG).setColor(0xff3366ff);
    }

    public void onEyeFrame(GraphicsDevice device, VREye eye, Camera camera) {
        device.draw(cube, material, Matrix4.translation(0, 0, -2));
    }

    public void onDispose(GraphicsDevice device) { }
});
vr.setContinuous(true); // head tracked scenes want continuous rendering

Form f = new Form("VR", new BorderLayout());
f.add(BorderLayout.CENTER, vr);
f.show();

setStereo(false) switches to a single centered viewpoint (VREye.CENTER) at runtime, useful for a non-headset preview of the same scene. setPosition(x, y, z) moves the viewer through the world; setClearColor(argb) sets the background.

A stereo scene on an Android phone; the offset between the eye views is the depth cue

20.3. Head tracking

VRView starts its HeadTracker when the component appears and stops it when it leaves the screen; the sensors draw power only between the two. The tracker fuses the gyroscope with the accelerometer (tilt reference) and, when present, the magnetometer (yaw reference) through a complementary filter - tune the blend via getHeadTracker().getFilter().setGyroWeight(float).

Call recenter() to make the current view direction the new "straight ahead," keeping pitch and roll - the standard VR recentering gesture. On devices without a gyroscope the tracker falls back to accelerometer tilt: pitch and roll stay accurate but yaw has no stable reference. HeadTracker.isSupported() reports whether any usable motion hardware exists; without it the orientation simply stays put and the scene still renders.

20.4. The 360 photo viewer

Media360View displays equirectangular panoramas - the format produced by 360 cameras and phone panorama modes, where longitude maps to X and latitude to Y:

Media360View pano = new Media360View();
pano.setImage(EncodedImage.create("/panorama.jpg"));
f.add(BorderLayout.CENTER, pano);
An equirectangular panorama in side-by-side stereo

Dragging looks around with the grab-the-world gesture; setHeadTrackingEnabled(true) adds gyroscope look composed with the drag, and setStereo(true) renders side by side for cardboard-style viewers. Photo spheres are captured from a single point, so stereo intentionally uses zero eye separation - there is no parallax information in the image to reproduce. setYaw/setPitch position the view programmatically and reset() returns to straight ahead.

20.5. 360 video and dynamic content

There is no platform path from media frames to GPU textures yet, so 360 video isn’t supported directly. TextureSource is the extension point for dynamic content: the view calls createTexture once on the render thread and updateTexture before every frame, so procedurally animated panoramas work today and a future media-to-texture bridge can plug in without API changes.

pano.setTextureSource(new TextureSource() {
    public Texture createTexture(GraphicsDevice device) {
        return device.createTexture(1024, 512, initialPixels);
    }
    public boolean updateTexture(GraphicsDevice device, Texture texture) {
        // mutate pixels and re-upload as needed; return true when changed
        return false;
    }
    public void dispose(GraphicsDevice device) { }
});

20.6. The simulator

Both VRView and Media360View run in the simulator through the desktop 3D backend, and the Simulate menu’s Motion / Gesture Simulation window feeds synthetic sensor data to the HeadTracker, so head-tracked scenes are drivable from the desktop. Drag-look in Media360View works with the mouse as it does with a finger.

20.7. Limitations and notes

  • Lens distortion: the stereo output is undistorted side-by-side rendering. Barrel distortion for lens-based viewers requires an offscreen render pass the portable GPU API doesn’t expose yet, so there is no distortion setting to configure.

  • Battery: continuous rendering plus live sensors is the most power-hungry mode a phone UI can run; stop or pause the view whenever it isn’t visible (the component does this automatically on hide).

  • Coordinates: the world frame matches the AR and 3D graphics APIs (meters, right-handed, Y up, -Z forward), letting content and math move between the three without conversion.

21. Game Development

Codename One is a general purpose UI toolkit, but it ships with a package built specifically for games: com.codename1.gaming. It gives you the things games need — a tight update loop, sprite primitives, pollable input, low latency sound effects and rigid body physics — and renders on the GPU through the com.codename1.gpu package. Everything written here runs unchanged on every Codename One target, including iOS.

This chapter covers the real time game surface. For the "casual game" approach — building game elements out of regular Component`s and letting the layout system render them — the techniques in the Chapter 12, Animations chapter are often a better fit. Reach for `com.codename1.gaming when you want a frame driven loop and GPU sprite rendering.

21.1. Sample games

The samples project ships several runnable starting points. Each is a single file built entirely from art generated at runtime — no asset files — so you can copy one and start changing it immediately. The rest of this chapter draws on them to show how the pieces fit together.

Casual arena demo
Side scroller demo
3D world demo

CasualGameSample
A top-down arena: fly a thruster ship with the on-screen joystick, scoop up spinning gems and dodge drifting asteroids. Many sprites in a Scene, per-sprite animation, collision, particle bursts and a twinkling starfield.

ScrollerGameSample
A side-scroller: an AnimatedSprite runner with a walk cycle that flips to face its direction, a sun/cloud/hill parallax backdrop, hand-rolled jump physics, and a joystick + JUMP button.

Gaming3DDemoSample
A 3D world: a spinning multi-cube monument on a textured grid ground, ringed by colored Model buildings and billboarded coins, lit and orbited by a perspective camera.

Two more samples show how far the same 2D primitives stretch — one flat, one faked into 3D:

  • CardGameSample — a 2D Memory (Concentration) match. Each card is a Sprite in a grid; flipping one animates its horizontal scale through zero and swaps the image at the thin point, and taps are hit-tested against sprite bounds through GameInput. A compact illustration of flat 2D play with no camera tricks at all. Dissected in Section 21.6, “Case study: The card game (flat 2D)”.

  • BoardGameSample — faux-3D checkers on an isometric board. Every tile and piece is still a flat Sprite, but laying them out in an isometric (2:1 diamond) projection and raising the pieces off their tiles with a drop shadow gives a convincing 3D look without any GPU 3D, perspective camera or models. The screen pixel-to-board-cell mapping (and its inverse, for picking) is the same math behind any isometric strategy or tycoon game; it also implements the full move/jump/chain /crown rules and a small AI so you can play it solo. Dissected in Section 21.7, “Case study: Faux-3D checkers (isometric projection)”.

The action samples are driven by the joystick and buttons in Section 21.4, “On-screen touch controls”, so they’re equally playable on a touch device and in the desktop simulator (where the arrow keys and mouse stand in for the stick); the card and board games are pure tap.

21.2. The game loop: GameView

GameView is the heart of the package. It’s a GPU surface (a RenderView) that runs the frame loop for you — a display link on device, the dependency-free software rasterizer in the simulator — so there’s no EDT busy loop and no frame rate to manage. You build your world by adding Sprite`s to its `Scene, advance the game in update(double), and the view draws the scene every frame.

The dt passed to update is the wall clock time since the previous frame. Multiplying movement by it keeps the game running at the same speed regardless of the actual frame rate ("delta timing"). Notice there’s no draw method — positioning sprites is all you do; the GPU renderer handles the rest (see Section 21.5, “Sprites”).

21.2.1. Lifecycle

start() begins continuous rendering, stop() halts it, and pause()/resume() suspend update while the view keeps drawing the last frame. start() is safe to call before or after the form is shown.

setClearColor(int argb) sets the background the view is cleared to each frame.

21.2.2. Fixed timestep and interpolation

Variable time steps are simple but make physics nondeterministic. Call setFixedTimestep(seconds) to have update invoked at a fixed interval instead; the loop accumulates real time and may call update several times in one frame to catch up (capped to avoid a "spiral of death" after a long pause). The leftover fraction is available from getInterpolationAlpha() (0 to 1) so you can interpolate rendered positions between physics states:

game.setFixedTimestep(1.0 / 120.0);   // step physics at a steady 120Hz

21.2.3. Threading

update(double) runs on the render thread, together with drawing. Keep it non-blocking — offload asset loading, networking or other long work to a background thread and hand the result back with CN.callSerially.

21.3. Input: GameInput

Games usually want to ask "is the left key down right now?" rather than handle a stream of events. GameInput, obtained from GameView.getInput(), captures the view’s key and pointer events and exposes them as state. There are two flavors:

  • Level state, true for as long as the input is held: isKeyDown(int), isGameKeyDown(int gameAction), isPointerDown().

  • Edge state, true only during the single frame the transition happened: wasKeyPressed(int), wasKeyReleased(int), wasPointerPressed(), wasPointerReleased(). Edges are cleared at the end of each frame, after update has run.

isGameKeyDown works with the device independent game actions Display.GAME_UP, GAME_DOWN, GAME_LEFT, GAME_RIGHT and GAME_FIRE, so directional input works the same across keyboards and devices. Pointer coordinates are reported relative to the `GameView’s top left.

21.4. On-screen touch controls

A keyboard is fine in the simulator, but a phone has no arrow keys. Every GameView comes with a TouchControls (from getControls()) that draws an on-screen virtual joystick and buttons and feeds them into the same GameInput — so the input code you already wrote for the keyboard works untouched on touch devices.

  • The joystick reports analog direction through GameInput#getAxisX() / getAxisY() (each -1 to 1), and at the same time presses the digital game actions GAME_LEFT / GAME_RIGHT / GAME_UP / GAME_DOWN once it crosses its dead zone, so isGameKeyDown(…​) keeps working.

  • A button holds down a key code while it’s pressed, so isKeyDown(int) and wasKeyPressed(int) report it exactly like a hardware key. Map it to a game action with Display.getInstance().getKeyCode(Display.GAME_FIRE), or to any key code of your own.

Anchor controls to a corner of the view’s safe area with the alignment constants (TouchControls.LEFT / CENTER / RIGHT and TOP / CENTER / BOTTOM) and a margin. The framework keeps them clear of notches and home indicators and repositions them automatically when the view resizes or the device rotates — you never recompute coordinates:

TouchControls c = getControls();

// analog stick, bottom-left, 80px radius, 24px in from the safe-area edges
c.addJoystick(80, TouchControls.LEFT, TouchControls.BOTTOM, 24);

// a JUMP button, bottom-right, mapped to GAME_FIRE
int jumpKey = Display.getInstance().getKeyCode(Display.GAME_FIRE);
c.addButton(jumpKey, 55, TouchControls.RIGHT, TouchControls.BOTTOM, 30)
        .setLabel("Jump").setColor(0xc0ff7043);

// then read input exactly as you would for a keyboard:
float ax = getInput().getAxisX();                 // analog steering
boolean jump = getInput().wasKeyPressed(jumpKey);  // or GAME_UP, etc.

The joystick (bottom-left) and JUMP button (bottom-right) are visible in the sample screenshots above. There are also absolutely positioned overloads (addJoystick(centerX, centerY, radius)), and setVisible(false) hides the overlay — handy when a real keyboard is attached. Multi-touch is supported, so the player can steer and press a button at once.

21.5. Sprites

A Sprite is a lightweight data holder: an image plus position, rotation, scale, a normalized anchor and an ARGB tint. It describes what and where; a SpriteRenderer turns it into a GPU textured quad each frame using the com.codename1.gpu package — an orthographic camera mapping one world unit to one pixel, a com.codename1.gpu.Material.Type#SPRITE material and alpha blending. Because a sprite never touches the GPU directly you create one with just an com.codename1.ui.Image; the renderer uploads and caches the matching texture on demand.

getX()/getY() is the location of the anchor point (the image center by default), and rotation and scale pivot around that anchor.

getBounds() returns the axis aligned bounding box and intersects(Sprite) does a quick box overlap test, handy for broad phase collision before you involve physics.

21.5.1. Scenes

A Scene is a z-ordered collection of sprites with an optional camera offset, drawn for you by the renderer. Sprites are drawn from lowest to highest zOrder, so higher z-order sprites appear on top; the camera offset (setCamera(int, int)) scrolls the whole scene. GameView.getScene() is the scene the view draws.

You don’t need a GameView to render sprites: a SpriteRenderer is itself a com.codename1.gpu.Renderer, so you can host one in a plain RenderView.

21.5.2. Sprite sheets and animation

SpriteSheet slices one atlas image into a grid of equally sized frames, cutting and caching each frame on first use (cutting a sub image copies pixels, so caching matters).

AnimatedSprite is a Sprite that cycles through a sequence of frames over time; the scene advances it every frame, so adding it to the scene is all you need.

A six-frame run cycle laid out as a sprite sheet

A sprite sheet is just a strip (or grid) of frames like the run cycle above: the arms and legs swing a little further each frame, and playing them in sequence reads as motion. AnimatedSprite also accepts a plain com.codename1.ui.Image[] when you build the frames yourself, which is what `ScrollerGameSample’s runner does:

AnimatedSprite hero = new AnimatedSprite(runFrames, 0.09);   // Image[6], 90ms each
hero.play();
getScene().add(hero);

// in update(double dt):
if (moving) {
    hero.play();
    hero.setScale(facing, 1);     // facing is -1 (left) or 1 (right)
} else {
    hero.pause();
    hero.setCurrentFrame(0);      // standing pose
}

Setting a negative scale flips the sprite horizontally, so a single set of frames faces both ways — no mirrored art needed (sprites are drawn double-sided for exactly this reason). play(), pause(), stop(), setLooping(boolean) and setCurrentFrame(int) control playback.

21.6. Case study: The card game (flat 2D)

The Memory card game mid-flip: two matched pairs and one card face up

CardGameSample is Memory (Concentration): sixteen face-down cards in a four-by-four grid, tap two and they stay up when they match, flip back when they don’t. It’s the smallest complete example of turn-based 2D play with com.codename1.gaming — no joystick, no physics, no camera — which makes the three techniques it does use easy to see in isolation.

One sprite per card. Each card is a Sprite positioned at its grid slot. There are exactly two images at any moment: the shared card-back image and the card’s own face (all generated at runtime with Image.createImage and Graphics calls, so the sample needs no assets). Changing what a card shows is just sprite.setImage(…​).

A flip you can fake with scale. A real card flip is a 3D rotation, but squeezing the sprite’s horizontal scale through zero and swapping the image at the thin point reads exactly the same to the eye:

// in the card's per-frame animation; flip runs 0 -> 1 over ~0.4s
flip += dt * 5;
if (flip >= 1) {
    // Finish the animation: snap back to full width and stop. Without
    // this, abs(1 - 2 * flip) keeps climbing past 1 and the card grows
    // wider on every later frame instead of settling.
    sprite.setImage(face);
    sprite.setScale(1f, 1f);
    return;
}
float sx = Math.abs(1f - 2f * (float) flip);   // 1 -> 0 -> 1
if (flip >= 0.5 && !showingFace) {
    showingFace = true;
    sprite.setImage(face);     // swap sides while the card is edge-on
}
sprite.setScale(Math.max(0.05f, sx), 1f);

This is the same negative/positive scale trick the scroller uses to flip its runner, driven over time to play as an animation.

Polled taps against sprite bounds. There are no buttons to wire up; update(double) polls the GameInput edge state and hit-tests the tap against each card’s bounding box:

protected void update(double dt) {
    // ... advance flip animations, count down the mismatch timer ...
    if (getInput().wasPointerPressed()) {
        int px = getInput().getPointerX();
        int py = getInput().getPointerY();
        for (int i = 0; i < cards.length; i++) {
            Rectangle b = cards[i].sprite.getBounds();
            if (b.contains(px, py)) {
                flipUp(cards[i]);
                break;
            }
        }
    }
}

The rest of the sample is a small state machine living in plain fields: the first and second face-up cards, a mismatchTimer that counts down in update before flipping a failed pair back, and a moves counter surfaced through the form title. Note the timer: in a game loop you don’t schedule callbacks, you subtract dt from a countdown field each frame and act when it crosses zero.

21.7. Case study: Faux-3D checkers (isometric projection)

Isometric checkers mid-game with a selected piece and a highlighted destination

BoardGameSample plays a full game of checkers — moves, forced captures, chained jumps, crowning, and a small AI opponent — on a board that looks convincingly 3D — yet there is no perspective camera and no Model anywhere: every tile and piece is a flat Sprite. The 3D impression comes entirely from where the flat art is placed, a technique (isometric or "2.5D" projection) that powered decades of strategy and tycoon games and still works just as well on a GPU sprite pipeline.

The projection. Board cell (r, c) maps to screen pixels by laying the grid out as 2:1 diamonds — columns step right-and-down, rows step left-and-down:

private float tileCenterX(int r, int c) {
    return originX + (c - r) * (tileW / 2f);
}

private float tileCenterY(int r, int c) {
    return originY + (c + r) * (tileH / 2f);   // tileH = tileW / 2
}

Picking is the inverse. Because the mapping is linear it inverts with two divisions, turning a tap back into a board cell — no per-tile hit testing required:

private int[] pick(int px, int py) {
    float a = (px - originX) / (tileW / 2f);   // = c - r
    float b = (py - originY) / (tileH / 2f);   // = c + r
    int c = Math.round((a + b) / 2f);
    int r = Math.round((b - a) / 2f);
    if (r < 0 || r >= N || c < 0 || c >= N) {
        return null;                            // tap missed the board
    }
    return new int[]{r, c};
}

Depth from z-order. Cells further down the screen must draw over the cells behind them, and within one cell the stacking is tile, then shadow, then selection/move markers, then the piece. Both rules collapse into a single number — (r + c) strides the cells from back to front, and a small offset layers the sprites within a cell:

// shadow sits flat on the tile, the piece floats above it
addDynamic(shadow, cx, cy + tileH * 0.10f, (r + c) * 4 + 1, 0.5, 0.5);
addDynamic(piece,  cx, cy - tileH * 0.30f, (r + c) * 4 + 3, 0.5, 0.62);

That pair of lines is also the whole "3D height" illusion: the piece is drawn raised off its tile while its shadow stays put, and the eye reads the gap as elevation. The piece art helps — an elliptical top face over a darker side rim, again just fillArc calls on a runtime-generated image.

Rebuild, don’t mutate. After every move the sample throws away all its dynamic sprites (pieces, shadows, markers) and recreates them from the board[][] array in one syncPieces() pass. With a few dozen sprites this costs nothing, and it keeps the scene a pure function of the game state — there is no way for the display to drift out of sync with the rules, which eliminates the whole class of "the board shows X but the game thinks Y" bugs. The game logic itself (destinations, forced captures, chaining, crowning) never touches a sprite; it reads and writes the int[][] board only.

When fake 3D stops being enough — you want the camera to move through the world rather than look at it — the next section covers the real thing.

21.8. 3D and perspective

GameView renders on the GPU through the com.codename1.gpu package, so the same scene can be shown in 3D. Every view has a GameCamera that starts in 2D mode; switching it to perspective turns flat sprites into camera-facing billboards in a 3D world and lets you draw 3D meshes alongside them. That’s the basis for deeper gameplay modes — an over-the-shoulder racer, an isometric or top-down view with real depth, a 2.5D shooter.

The 3D sample: a textured grid ground, stacked cube models, ringed buildings and billboarded coins

Gaming3DDemoSample (above) is built entirely from the primitives this section describes — a textured ground plane, lit cube Model meshes, and billboarded Sprite coins and trees, all orbited by a perspective camera.

21.8.1. Perspective camera and billboard sprites

Call GameCamera#setPerspective(float, float, float) and position the camera with setPosition / setTarget. Once in perspective mode a Sprite is placed in world space with Sprite#setPosition(double, double, double) and is drawn as a billboard that always faces the camera, so your existing 2D art keeps working in 3D:

getCamera()
    .setPerspective(60, 0.1f, 500f)   // vertical FOV, near, far
    .setPosition(0, 6, 12)            // eye
    .setTarget(0, 0, 0);             // look-at

Sprite tree = new Sprite(treeImage);
tree.setPosition(0, 1, 0);            // world coordinates, y up
tree.setSize(2, 4);                   // size is now in world units, not pixels
getScene().add(tree);

World coordinates are right-handed with y up (the com.codename1.gpu convention), and a sprite’s size is world units rather than pixels — use Sprite#setSize(float, float) or Sprite#setScale(float) to pick a world-space size. Move the camera each frame in update(double) to follow the player.

21.8.2. 3D models

Model draws a real 3D mesh — a com.codename1.gpu.Material plus a position, rotation, and scale. Because GPU meshes need the com.codename1.gpu.GraphicsDevice, build them in GameView#onSetup(com.codename1.gpu.GraphicsDevice), the render-thread hook that runs once before the first frame:

protected void onSetup(GraphicsDevice device) {
    Mesh cubeMesh = Primitives.cube(device, 1f);
    Material gold = new Material(Material.Type.PHONG).setColor(0xffffcc33).setShininess(32);
    crate = new Model(cubeMesh, gold).setPosition(0, 0.5f, 0);
    addModel(crate);

    getLight().setDirection(-1, -1, -0.5f);   // shade the lit material
}

protected void update(double dt) {
    crate.setRotation(0, crate.getRotationY() + (float) (60 * dt), 0);  // spin
}

onSetup is also where you load real assets with com.codename1.gpu.GltfLoader (glTF .glb/.gltf models). Models are drawn opaque and depth-written before the alpha-blended billboards, and the billboards depth-test against them, so closer geometry correctly hides what’s behind it. GameView#getLight() controls the directional light that shades lit material types.

21.8.3. Meshes and textures

The built-in meshes are com.codename1.gpu.Primitives#cube(GraphicsDevice, float) and Primitives#quad(GraphicsDevice, float); a quad rotated flat makes a ground or a wall. Model#setScale(float, float, float) reshapes one cube mesh into crates, pillars or buildings of any proportion, so a whole skyline can share a single mesh. Lit materials multiply their base color by a texture, so you can detail a surface — a grid on the ground for depth, say — by drawing an com.codename1.ui.Image and uploading it with com.codename1.gpu.GraphicsDevice#createTexture(com.codename1.ui.Image):

Mesh ground = Primitives.quad(device, 64f);
Texture grid = device.createTexture(makeGridImage());      // any Image you draw
Material mat = new Material(Material.Type.LAMBERT)
        .setColor(0xff3f7d4f).setTexture(grid);
Model floor = new Model(ground, mat).setRotation(-90, 0, 0); // lay the quad flat
addModel(floor);

// one cube mesh, scaled into buildings of varying height:
Model tower = new Model(cubeMesh, brick).setScale(1.5f, 4f, 1.5f).setPosition(9, 2, 0);
addModel(tower);

For richer geometry, load glTF (.glb/.gltf) assets with com.codename1.gpu.GltfLoader from onSetup.

21.9. Low latency audio: SoundPool

Music and video use the regular MediaManager, but rapid overlapping sound effects — gunshots, coins, footsteps — need a different tool. SoundPool loads short clips once and triggers them with minimal latency, mixing several at the same time.

play returns a voice id (or -1 if the pool is momentarily exhausted — it never blocks on the hot path). The parameters are: volume 0 to 1, pan -1 (left) to 1 (right), rate playback speed/pitch (1.0 is normal), and loop (0 once, -1 forever, n extra repeats). stop, pause, resume, stopAll, autoPause and autoResume manage playback; release() frees the pool.

On platforms with a purpose built low latency audio engine — Android’s SoundPool, iOS AVAudioEngine, the desktop javax.sound.sampled mixer and WebAudio in the browser — the pool uses it directly with full volume/pan/pitch support. Where none exists it falls back to a MediaManager based pool that still works everywhere but has higher latency and ignores pan and rate. isNativeAccelerated() tells you which path is active.

To react as a sound finishes — chaining clips, recycling a game object when its effect ends — register a VoiceListener. It’s called on the EDT with the voice id as each voice completes. Not every native engine can report completion, so guard with isVoiceCompletionSupported():

if (sfx.isVoiceCompletionSupported()) {
    sfx.setVoiceListener(new VoiceListener() {
        public void onComplete(int voiceId) {
            onEffectFinished(voiceId);
        }
    });
}
Load your sounds once, up front — ideally on a background thread with SoundPool.loadAsync(…​) — and keep the SoundEffect references for the life of the game. Decoding on the fly defeats the purpose.

21.10. Physics

The com.codename1.gaming.physics package adds 2D rigid body physics. It’s an idiomatic wrapper around a pure Java port of the well known Box2D engine, so it runs on every platform — including iOS, where it’s translated to C with no native code.

21.10.1. Units: Pixels, meters and the y-axis

Box2D is tuned for objects a few meters across, so the world works in meters internally while exposing pixels to you. The conversion is governed by setPixelsPerMeter(float) (default 30). The wrapper also flips the y-axis — Box2D points y up, the screen points y down — so your code stays in screen coordinates. The practical consequence: a positive gravity y pulls bodies down the screen.

21.10.2. Worlds and bodies

Create a PhysicsWorld, populate it with PhysicsBody objects, and step it once per frame from your update:

PhysicsWorld world = new PhysicsWorld(0, 900);   // gravity 900 px/s^2 downward

// a STATIC floor that never moves
world.createBox(160, 460, 320, 40, BodyType.STATIC);

// a DYNAMIC crate affected by gravity and collisions
PhysicsBody crate = world.createBox(160, 0, 32, 32, BodyType.DYNAMIC);
crate.setRestitution(0.4f);   // bounciness
crate.setFriction(0.5f);

// in update(double dt):
world.step((float) dt);

Bodies come in three kinds, the BodyType values: STATIC (immovable — ground, walls), KINEMATIC (moved by you via velocity, unaffected by forces) and DYNAMIC (fully simulated). Bodies can be boxes, circles or polygons (createBox, createCircle, createPolygon) and respond to setLinearVelocity, applyForce, applyLinearImpulse, applyTorque, setBullet (continuous collision for fast objects), setFixedRotation and more.

For an arbitrary outline, createShape(x, y, shape, type) takes a Codename One com.codename1.ui.geom.Shape — typically the same com.codename1.ui.geom.GeneralPath you would draw with — and builds the collision fixtures from it, flattening any Bezier curves. Each subpath becomes a fixture (so a figure with several outlines is a compound body); a closed convex subpath of up to eight points becomes a solid polygon usable by any body type, while a concave, larger, or open subpath becomes a one-sided edge chain (ideal for static terrain). This lets the hit-shape and the drawn shape come from one piece of geometry:

GeneralPath ramp = new GeneralPath();
ramp.moveTo(0, 0);
ramp.lineTo(240, 0);
ramp.lineTo(240, 80);
ramp.closePath();
world.createShape(40, 380, ramp, BodyType.STATIC);   // collide with exactly what you draw

21.10.3. Driving sprites from physics

A body can drive a sprite directly. Sprite implements PhysicsLinkable, so linking the two means world.step(…​) updates the sprite’s position and rotation automatically (in pixels, screen space) — and the scene draws it there:

Sprite crateSprite = new Sprite(crateImage);
crate.setLinkedSprite(crateSprite);
getScene().add(crateSprite);

// in update(double dt):
world.step((float) dt);   // moves crate, which moves crateSprite, which the scene draws

21.10.4. Collisions

Register a ContactListener to be told when bodies start and stop touching. Callbacks fire from inside step(…​) — on the game loop thread — so you can read and update game state directly, but you must not create or destroy bodies during the callback; defer that until after step returns.

If you need a feature the wrapper doesn’t expose, PhysicsWorld.getNativeWorld() and PhysicsBody.getNativeBody() give you the underlying engine objects (which work in meters, y up).

21.10.5. Joints

Joints constrain how two bodies move relative to each other — hinges, ropes, axles, welds. PhysicsWorld creates them in pixel coordinates and returns a PhysicsJoint handle:

// a hinge the two bodies pivot around (anchor in pixels)
world.createRevoluteJoint(bodyA, bodyB, pivotXpx, pivotYpx);
// a fixed-length link, like a rod between two crates
world.createDistanceJoint(bodyA, bodyB, ax, ay, bx, by, 0f, 0f);
// drag a body toward the finger -- great for "pick up and throw"
PhysicsJoint drag = world.createMouseJoint(ground, body, px, py, 1000f);
// ... while the finger moves:
drag.setTarget(getInput().getPointerX(), getInput().getPointerY());
// ... on release:
drag.destroy();

createWeldJoint and createPrismaticJoint (a sliding axis) round out the set.

21.10.6. Debugging physics

When sprites and bodies drift apart it helps to see the raw simulation. Call world.debugDraw(Graphics) to overlay every fixture, joint and contact straight onto a Codename One Graphics — shapes, centers of mass and bounding boxes — in pixel space, with the y-axis flip already applied. Choose what to show with setDebugDrawFlags(shapes, joints, boundingBoxes), and soften the translucent shape fill with setDebugFillAlpha(alpha). Because it draws to an ordinary Graphics you can render it over a regular Codename One component, not only a GameView.

21.11. Putting it together: Anatomy of a game

Every sample follows the same shape, which is the recipe for your own game:

  1. Subclass GameView and, in its constructor, create your Sprite and AnimatedSprite objects and add them to getScene().

  2. Allocate GPU resources in onSetup(GraphicsDevice) — meshes, textures and 3D Model objects — because those need the device.

  3. Add controls (a joystick and buttons) once the view has a size.

  4. Advance the world in update(double dt): read getInput(), move sprites or step a PhysicsWorld, scale all movement by dt, and trigger SoundPool effects.

  5. Show it: add the view to a Form and call start().

The samples project has several runnable examples to copy from:

  • CasualGameSample — sprites, per-sprite animation, collision and particle bursts in a top-down arena (see Section 21.1, “Sample games”).

  • ScrollerGameSample — an AnimatedSprite walk cycle, a parallax backdrop and a scrolling Scene camera.

  • Gaming3DDemoSample — lit cube Model objects, a textured ground and billboarded sprites under a perspective camera.

  • GamingDemoSample — the physics tour, built on a PhysicsWorld with a floor and walls, tap-to-drop balls that bounce (each Sprite linked to its body) and a SoundPool blip whose pitch varies per drop.

  • CardGameSample — a flat 2D Memory match: a grid of card Sprite objects with a flip animation and tap hit-testing.

  • BoardGameSample — faux-3D checkers: flat sprites placed in an isometric projection, with screen-to-cell picking, full rules and a small AI.

Each is a single self-contained file that generates its art at runtime, so it needs no assets — a compact starting point you can copy and reshape.

21.12. Performance notes

  • Keep update non-blocking — it runs on the render thread. Load assets off the render thread and hand them back with CN.callSerially.

  • Reuse images: the renderer caches one GPU texture per Image, so sharing an image across many sprites uploads it once. Cache cut sprite frames (use SpriteSheet, which does it for you) rather than re-slicing every frame.

  • Load every SoundEffect up front and reuse it.

  • Tune setPixelsPerMeter so your moving bodies are on the order of a meter (tens of pixels) in size; bodies far larger or smaller than that make the simulation unstable.

The physics engine in com.codename1.gaming.physics.box2d is a derived work of JBox2D (a Java port of the Box2D engine), used under the BSD 2-Clause license; see the project NOTICE file for attribution.

22. The Game Builder

The Game Builder is a visual level / map editor for the com.codename1.gaming API. You lay out a game world — tiles, characters, items, the camera — set per-object behavior values, preview it live, and Build it into your project as a data file plus a small Java companion that the runtime plays. It’s to games what the GUI builder is to forms.

This chapter covers the whole feature: the underlying model, the editor UI, building a level, defining game rules, the supported game types, and the Maven workflow.

22.1. How it fits together

A game built with this tool has three layers:

LayerRoleWho authors it

Level data (*.game)

A JSON description of the scene: grid, layers, placed elements + their properties, camera, lights.

The Game Builder (visual)

Companion class (*.java)

A thin GameSceneView subclass that loads the data and holds your game logic.

Generated once, then you edit the onUpdate body

Gaming engine (com.codename1.gaming)

Renders and runs the scene on the GPU, with pollable input, sound and optional Box2D physics.

The framework

The key idea: the editor produces data, your companion adds rules, the engine runs it. Because the level is data you can re-edit it any time without touching your code, and your onUpdate logic is preserved across regeneration.

22.1.1. The level model — com.codename1.gaming.level

ClassPurpose

GameLevel

The saveable level: getMode() (MODE_2D / MODE_3D / MODE_BOARD), grid, layers(), elements(), camera, lights(), getTerrain(). load(json) / toJson().

GameElement

A placed thing: id, name, assetId, layer, a transform and a typed property bag (getInt/getDouble/getString/getBoolean). It’s not a Sprite, so the same model works in 2D and 3D.

Layer

A named band: KIND_TILE (an assetId-per-cell grid), KIND_ENTITY, KIND_MODEL; plus isVisible() / isLocked().

AssetCatalog / AssetPack / AssetDef

Resolve an assetId to artwork and default properties. Packs ship as JSON.

GameSceneView

A turnkey GameView that plays a level: it realizes 2D/board sprites and, in 3D, sets up the perspective camera, lights and a Model per element. Put game logic in onUpdate(double dt).

IsoProjection, TileLayer, LevelLight, TerrainGrid

Board isometric math, fast single-tileset grids, and 3D lighting / terrain data.

Loading and playing a level at runtime is three lines:

GameLevel level = GameLevel.load(
        // Flat resource namespace on the device -- "/Level1.game", not
        // "/games/Level1.game", which resolves to null there. This is the
        // path the editor's generated companion uses.
        Display.getInstance().getResourceAsStream(MyScene.class, "/Level1.game"));
// load() only parses the pack definitions. resolveArt() is what loads the
// images, sheets and meshes they name, and the 2D constructor realizes
// sprites immediately -- so resolve before constructing, or the scene is
// built from placeholders and stays that way.
AssetCatalog catalog = AssetCatalog.load(packsJson);
catalog.resolveArt();
GameSceneView view = new GameSceneView(level, catalog);
form.add(BorderLayout.CENTER, view);
form.show();
view.start();

22.2. Getting started

Create a scene from a Java 17 Codename One project:

mvn cn1:create-game-scene -DclassName=com.example.game.Level1 -Dmode=2d

-Dmode is 2d (default), 3d or board. This writes common/src/main/resources/games/Level1.game (an empty level of that type) and common/src/main/java/com/example/game/Level1.java (the GameSceneView companion).

Open the editor bound to the project — the same way cn1:guibuilder launches the GUI builder:

mvn cn1:gamebuilder

The editor itself is delivered through Maven: the goal resolves the com.codenameone:codenameone-gamebuilder artifact from the Codename One Maven repository (the same mechanism that ships the Designer / CSS-compiler jar with the plugin), so there is no separate install step. The cn1:gamebuilder goal only applies to Java 17 Codename One projects.

22.3. The editor at a glance

The Game Builder editing a 2D platformer
  • Top bar — the project / scene dropdown (switch scenes, rename, or start a new scene of any game type), undo / redo, the grid and snap toggles, zoom, a text-size toggle (Small / Medium / Large), the </> button (opens the scene’s Java in your IDE), Live (play in place) and Build (save into the project).

  • Tool rail (left) — Select, Move, Brush, Fill, Erase, Pan.

  • Resizable panels — drag the thin divider on the inner edge of the left panel or the inspector to resize it.

  • Hierarchy — the scene card plus the layers. Each layer shows its item count, a visibility eye (hides the layer), a lock (prevents edits) and a ⋮ menu to rename or delete it; drag a placed object onto another layer (or above/below a sibling) to move or reorder it. Placed objects nest under their layer, and unique objects (the player) carry a UNIQUE badge.

  • Canvas — the editable stage is the bright-bordered rectangle with the grid; painting only happens inside it.

  • Asset Library (center-bottom) — pick a pack tab, then click a thumbnail to select an asset. Selecting a tile asset switches to a tile layer; selecting an actor switches to an entity layer, so the brush always works on the next click. Import (top-right) brings in your own artwork as a new asset.

  • Inspector (right) — scene settings when nothing is selected, or the selected object’s properties.

Because it’s a desktop application, every command is also available from the native menu bar (drawn by the OS), grouped by convention: File (New 2D / 3D / Board, Open Scene, Save), Edit (Undo, Redo, Delete Selection), View (Toggle Grid / Snap, Zoom In / Out, Show Generated Code), Tools (Select / Move / Brush / Fill / Erase / Pan), Layer (Add Layer) and Game (Play / Stop). The standard accelerators work too — Cmd/Ctrl+Z undo, Cmd/Ctrl+S save, Cmd/Ctrl+R play, Cmd/Ctrl+= / Cmd/Ctrl+- zoom. Every on-screen control also has a tooltip.

22.4. Building a level

  1. Choose a game type — scene dropdown → New scene → 2D Platformer, 3D Map or Board (or scaffold with -Dmode).

  2. Paint terrain — pick the Brush, click a tile in the Asset Library, then click/drag inside the stage. Fill flood-fills a region; Erase clears cells.

  3. Place characters and items — click an actor asset and click on the stage to stamp it. With Snap on, actors land on grid cells. The Player is unique — stamping it again moves it.

  4. Organize with layers — placed objects nest under their layer in the hierarchy, so you can always see which band each one is on. The eye hides a layer while you work (its name dims), the lock prevents edits to it, the chevron collapses its children, and + adds a layer. To move a selected object to a different layer (changing its draw order), use the Layer picker in the Inspector’s Transform section.

  5. Undo / redo every change with the toolbar arrows.

  6. Save with Build — it writes Level1.game and, the first time, the companion Level1.java.

22.5. Defining game rules

Game rules live in two complementary places.

22.5.1. Data — set in the Inspector

Editing a selected object’s properties

Select an object to edit its Transform (name, position, Layer dropdown) and its Behavior properties — the typed values that parameterize how it acts:

ObjectBehavior properties

Player / Hero

lives, jumpHeight

Coin / Gem

value

Enemy (slime)

speed, patrol

Door / Goal

target (next scene)

Torch

lightRadius

Card / Token / Dice

suit/rank, player/cell, sides

These defaults come from the asset — the Inspector’s read-only Asset section shows the id, kind, size and unique flag, and Edit asset defaults… lets you change an asset’s default size and properties (for example, tune ground/coin). Use Add property… to attach your own game metadata (hitPoints, value, …) to a single object, readable at runtime via el.getInt(…​) / el.getDouble(…​). This is the player marks which element the preview and arrow keys drive (otherwise a player/hero/spawn asset is used).

Scene-wide rules are set in the Inspector when nothing is selected: gravity, background theme, grid Cols / Rows / Tile size and the Mode. These are saved into the .game file and are available at runtime on each GameElement.

22.5.2. Logic — written in the companion

The behavior values are data; the behavior itself is code you write in the generated companion’s onUpdate(double dt). GameSceneView realizes every element into a sprite whose getUserData() is the source GameElement, so you read the properties you set in the editor:

@Override
protected void onUpdate(double dt) {
    // The generated companion switches the built-in arcade behaviour on, and
    // that already patrols enemies and collects pickups. A scene doing the
    // work itself calls setArcadeBehavior(false) in its constructor -- with
    // both running, the built-in patrol and the step below cancel out and
    // the slime stalls.
    GameInput in = getInput();
    Scene scene = getScene();
    // The editor names a stamped object after the asset's display name, so
    // the generated companion has a field like duke1 rather than "player".
    // Look the sprite up by asset id instead of assuming a field name.
    Sprite player = findByAsset("player");
    // Backwards, because collecting a coin removes it from the scene and a
    // forward loop would step over the next sprite.
    for (int i = scene.size() - 1; i >= 0; i--) {
        Sprite s = scene.get(i);
        // Tile sprites carry a Layer in their user data, not a GameElement, so
        // a plain cast fails on the first frame of any scene with painted
        // tiles. elementOf() answers null for those. It also matters that this
        // is not a cast: ParparVM does not check them, so on iOS a bad cast
        // reads the wrong object's fields instead of throwing.
        GameElement el = elementOf(s);
        if (el == null) continue;
        switch (el.getAssetId()) {
            case "player" -> {
                if (in.isGameKeyDown(Display.GAME_RIGHT)) s.setX(s.getX() + 200 * dt);
                // jump height, lives, gravity... all read from el.getInt(...)/getDouble(...)
            }
            // speed is pixels per second, so scale it by the frame delta --
            // otherwise the patrol runs at whatever rate the device renders at
            case "slime" -> s.setX(s.getX() + el.getDouble("speed", 60) * dt);
            case "coin"  -> {
                if (s.intersects(player)) {
                    addScore(el.getInt("value", 10));
                    scene.remove(s);   // consume it, or it scores again every frame
                }
            }
        }
    }
}

Win/lose conditions, scoring, enemy AI and level transitions (using a door’s target) are all decided here. For rigid-body games use com.codename1.gaming.physics inside onUpdate. In short: the editor decides the layout and the numbers; your companion decides the behavior; the engine runs it.

22.6. Game types

The mode (chosen per scene) plus the asset pack determine how the same level model is realized:

ModeRealized asPacksUse for

MODE_2D

`Sprite`s in pixel space, tile grids

Platformer, Top-Down RPG

platformers, shooters, top-down adventures

MODE_BOARD

Sprite`s placed through an isometric `IsoProjection

Board & Card

board games, strategy, card games

MODE_3D

Model`s under a perspective `GameCamera, with LevelLight`s and a `TerrainGrid

3D Kit

3D maps, arena / tycoon layouts

Switch type from the scene dropdown’s New scene buttons or scaffold with -Dmode=2d|3d|board. In 3D mode the Inspector also exposes the camera rig, lighting and terrain; placed elements become meshes (import your own glTF via GltfLoader from the companion’s buildModels hook).

Editing a 3D map — the board autofits the canvas and zooms

In 3D mode the board is edited top-down (placement, terrain and lighting), and the grid autofits the canvas so it stays usable at any size and zoom. Each placed element reads as a single object on the grid rather than a flat sprite filling the board.

22.7. Live preview

Live preview — playing the level in a device frame

Press Live to play the level in place inside a device frame with a HUD. The preview ships default behaviors so common cases work without any code: in a 2D platformer the arrow keys move the player and Up / Space jumps (gravity and tile collision are simulated), coins/gems are collected for score, and enemies patrol and cost a life on contact. The on-screen hint shows the controls, and the SCORE / LIVES HUD is live. Press Stop to return to editing — playing never mutates the level.

For a 3D map the preview switches to a true perspective view — a receding ground grid with the placed meshes rendered as depth-sorted objects under an orbiting camera — so you see 3D gameplay rather than a flat top-down board:

The 3D live preview — perspective camera over the board

The in-editor preview is for quick iteration; for full-fidelity play on device, run the companion in your app, where the scene is rendered by the GPU-accelerated GameSceneView.

22.8. Build, save and run

Build writes the level to src/main/resources/games/<Scene>.game and (once) the companion <Scene>.java. To show the scene in your app, instantiate the companion and start() it:

Form game = new Form("Play", new BorderLayout());
// The generated scene takes the catalog as a constructor argument, and
// neither the scaffold nor the build produces one: it is yours to supply.
// Bundle the pack JSON your level references as an app resource and load
// it here. StarterPacks, which the editor uses, ships with the editor and
// is not on an application's classpath.
AssetCatalog catalog = AssetCatalog.load(
        Display.getInstance().getResourceAsStream(Level1.class, "/my-packs.json"));

// load() only parses the definitions. Nothing else loads the images,
// sprite sheets and meshes they name, so without this the scene renders
// placeholders and the sheets never animate.
catalog.resolveArt();
Level1 scene = new Level1(catalog);
game.add(BorderLayout.CENTER, scene);
game.show();
scene.start();

The level is plain data, so you can ship many .game files and load whichever level the player selects.

22.9. Maven goals

GoalPurpose

cn1:create-game-scene -DclassName=… -Dmode=2d|3d|board

Scaffold a .game + companion (Java 17 projects only).

cn1:gamebuilder

Launch the editor bound to the current project.

22.10. Extending

  • Asset packs are JSON ({"packs":[{"id","name","assets":[{"id","kind","w","h","color","unique","source","defaults"}]}]}) loaded by AssetCatalog.load. Add your own pack with real art.

  • Asset art follows the SVG/Lottie approach: reference source art per asset and let the build translate it to code/raster; where no art is supplied the catalog draws a recognizable placeholder so a level always renders.

  • Behaviors can be factored into reusable helpers driven entirely from the property bag, keeping levels data-only.

22.10.1. Importing your own art

To bring in custom artwork, click Import in the Asset Library header and pick an image.

Importing a custom image into the Custom asset pack

The image is added to a Custom pack as a new asset (a tile when a tile layer is active, otherwise an actor), selected and ready to paint or stamp immediately. When the editor is bound to a project, the picture is copied into src/main/resources/games/assets/ and recorded in games/custompack.json, so it survives reloading and ships with the project — the generated game resolves it through the same AssetCatalog.

23. Game Assets

Every visible thing in a Game Builder level — the ground a player walks on, a coin to collect, an enemy, a 3D crate — is drawn from an asset. This chapter explains what an asset is, how assets are organized into packs and resolved by the catalog, where their artwork comes from, and how to create your own (either by authoring a pack JSON entry or by importing an image inside the editor). It closes with a note on terrain materials, a related-but-separate concept, and pointers for finding more art.

The classes live in com.codename1.gaming.level: AssetDef, AssetPack and AssetCatalog.

23.1. What an asset is

An asset is a reusable template — an AssetDef — not a thing in your level. It says "this is what a coin looks like and what default values a coin has," and any number of coins in any number of levels reference that one definition by id.

An AssetDef carries:

FieldMeaning

getId()

The stable string key (for example, coin, player, ground). A placed element references this id, the catalog indexes by it, and imported art is filed under it.

getName()

The human-readable display name shown in the editor palette (for example, Coin). Defaults to the id.

getKind()

AssetDef.KIND_TILE (painted into a cell of a tile layer’s grid) or AssetDef.KIND_ACTOR (a free-form entity / model). isTile() is a convenience.

getWidth() / getHeight()

The natural pixel size of the art (default 32×32).

getColor()

A base ARGB colour used to render a flat placeholder until real artwork is available (default 0xff888888).

isUnique()

Whether a level should hold at most one of these (for example, the player / spawn point).

getSource()

An optional reference to the source art (a resource path, an SVG/Lottie key, or the assets/<id>.png path written for an imported image).

defaultProperties()

The default authoring property values copied onto a freshly placed element — a coin’s value, a player’s lives and jumpHeight, and more.

23.1.1. Asset (template) versus placed element (instance)

How a placed element relates to its asset template

Keep this distinction clear:

  • An asset (AssetDef) is the shared template. There is exactly one coin definition.

  • A placed element (GameElement) is one instance in a level. It stores its own position, size and property values, and references the template through GameElement#getAssetId().

When you drop a coin into a level, the editor creates a GameElement whose assetId is coin and copies the asset’s defaultProperties() into the element’s own property bag (see StarterPacks.addActor, which does exactly this with el.properties().putAll(def.defaultProperties())). You can then tweak that one coin’s value without affecting the template or any other coin. Changing the asset’s size or art, conversely, affects how every instance that references it’s drawn.

23.2. How assets are structured

Assets are grouped into packs, and packs are indexed by a catalog:

  • AssetPack — a named, ordered group of AssetDef`s (`add(def), get(id), assets()). The order is the order they appear in the palette. A GameLevel names the pack it draws from via GameLevel#getAssetPack().

  • AssetCatalog — holds one or more packs and answers two questions for any asset id: def(id) returns the AssetDef (used for kind, size and default props), and image(id) returns the artwork. Real art is supplied via setImage(id, image); until then image(id) returns a cached solid-colour placeholder sized from the def and tinted with its getColor(), so a level always realizes to something visible.

23.2.1. The built-in packs

The editor ships four starter packs in the classpath resource /gamebuilder-packs.json, loaded by StarterPacks.loadCatalog() (which calls AssetCatalog.load(InputStream)). The same catalog drives both the editor palette and a generated game’s runtime loader.

Pack idNameAsset ids

platformer

Platformer

ground, grass, brick, crate, ladder, spike (tiles); coin, gem, player, slime, door, flag (actors)

topdown

Top-Down RPG

floor, wall, water, tree, path (tiles); hero, npc, chest, torch (actors)

board

Board & Card

boardtile, start (tiles); card, token, dice (actors)

kit3d

3D Kit

block, crate3d, barrel, pillar, tree3d, rock, spawn (all actors)

StarterPacks.defaultPackFor(mode) picks kit3d for MODE_3D, board for MODE_BOARD and platformer otherwise.

The JSON format is {"packs":[{"id":..,"name":..,"assets":[ …​ ]}]}. Here is one real asset entry from the platformer pack — the player:

{ "id": "player", "name": "Player", "kind": "actor",
  "w": 28, "h": 32, "color": "#4D86FF", "unique": true,
  "defaults": { "lives": 3, "jumpHeight": 96 } }

Each JSON key maps onto an AssetDef field (see AssetDef.fromMap): kind is "tile" or "actor", w/h are the pixel size, color is #rrggbb, unique is a boolean, source is the optional art reference, and defaults becomes defaultProperties().

23.3. Where the art comes from

An AssetDef names an art format — TYPE_IMAGE, TYPE_SHEET or TYPE_MESH — and a getSource() art file. AssetCatalog.resolveArt() loads each asset’s art from that file into the catalog, so the same definition drives the editor palette and a shipped game.

23.3.1. Static images (TYPE_IMAGE)

A single .png/.jpg. resolveArt() decodes it; image(id) returns it and the level realizer wraps it in a com.codename1.gaming.Sprite. The starter packs ship one image file per asset (at the flat resource root, loaded as /<id>.png), so every starter sprite — ground, the player, the mountains — is a real file you can open in any editor and replace.

23.3.2. Sprite sheets (TYPE_SHEET)

One image laid out as a grid of equal frames, plus a frame size and rate (frameW, frameH, fps). resolveArt() builds a com.codename1.gaming.SpriteSheet; the realizer creates an animated com.codename1.gaming.AnimatedSprite that the scene advances each frame. The starter coin is a six-frame sheet, so it spins at runtime with no code; image(id) returns its first frame for the editor palette.

23.3.3. Meshes (TYPE_MESH)

A glTF/.glb model for a 3D level. resolveArt() keeps the file’s bytes and GameSceneView.buildModels loads them with com.codename1.gpu.GltfLoader, realizing a com.codename1.gaming.Model (and falling back to a primitive cube for an asset that has no mesh). Sprite sheets and glTF loading are existing Codename One features; the asset model just makes them first-class so the editor and runtime treat them as assets.

23.3.4. Imported art (the editor’s Import button)

The Import action opens the gallery (Display#openGallery) and hands the chosen image’s bytes to GameBuilder.registerImportedAsset(…​), which builds an image AssetDef (kind from the active layer), adds it to a Custom pack and persists the PNG to <gamesDir>/assets/ plus the pack definition to <gamesDir>/custompack.json. On the next launch ProjectIO.loadCustomPack reads them back, so an imported image survives a reload. Sprite sheets and glTF meshes are added the same way at the file level — drop the file into <gamesDir>/assets/ and add a pack entry with the matching type (and frame size for a sheet); resolveArt then loads it like any other asset. (A dedicated sheet/mesh import UI is a natural follow-up.)

23.3.5. Generating the starter images

The bundled starter PNGs are produced once by a build-time generator (StarterArtGenerator, which draws each sprite with Graphics through AssetArt) and committed as resources — there’s no art-drawing code in the shipped runtime. Re-run the generator after changing a starter sprite or pack.

In every case the catalog falls back to the flat-colour placeholder when an asset’s art is missing, so a level is never un-renderable.

23.4. Creating your own asset

There are two practical paths.

23.4.1. Path 1: Author a pack JSON entry

Add an entry to a pack’s assets array using the same format as the built-ins. For example, a collectible key:

{
  "id": "key",
  "name": "Key",
  "kind": "actor",
  "w": 24,
  "h": 24,
  "color": "#E0C040",
  "unique": false,
  "defaults": { "opens": "door", "value": 1 }
}

Field by field:

  • id — unique string key your placed elements and any art will reference. Required.

  • name — display name in the palette. Optional; defaults to the id.

  • kind — "tile" to paint into grid cells, "actor" for a free-form entity. Anything other than "tile" is treated as an actor.

  • w / h — natural pixel size. Optional; default 32.

  • color — #rrggbb placeholder colour used until real art is supplied. Optional.

  • unique — true to limit the level to one instance. Optional; default false.

  • defaults — a map of default property values copied onto each placed element.

  • source — the art file the runtime loads: a .png/.jpg (image, sheet) or a .glb/.gltf (mesh). Optional; the starter packs default it to <id>.png.

  • type — "image", "sheet" or "mesh". Optional; inferred from source (a .glb is a mesh) or frameW (a sheet) when omitted.

  • frameW / frameH / frames / fps — sprite-sheet frame size, frame count (0 = all) and playback rate, for a "sheet" asset.

You can add this to a copy of gamebuilder-packs.json, or build a pack in code with new AssetPack(id, name).add(new AssetDef(…​)) and register it with catalog.addPack(pack). To give it real art, point source at a bundled art file and call catalog.resolveArt(), or supply an image directly with catalog.setImage("key", image).

23.4.2. Path 2: Import an image in the editor

The no-code path: open your level in the Game Builder, select the layer you want the asset to belong to (a tile layer for tiles, an entity/actor layer otherwise), and use the Import action ("Import your own image as a new asset"). Pick a PNG (or other supported image), and the editor creates the AssetDef and files it under the Custom pack. Once bound to a project, the editor also saves the PNG and a pack-JSON entry under the project’s games/assets/ directory as described above. The new asset appears in the palette and is immediately placeable.

23.5. Terrain materials

3D terrain surfaces use a different, pluggable concept: materials, not sprite assets. A Material (com.codename1.gaming.level.Material) describes a surface — a base colour, whether it’s solid (blocks movement), a friction multiplier, and an optional artId for textured rendering — and is referenced everywhere by its string id.

Materials are resolved through MaterialRegistry, a process-wide registry that ships six built-ins (MaterialRegistry.GRASS, ROAD, STONE, SAND, WATER, DIRT). WATER is registered as solid. Applications add their own without changing the level format:

MaterialRegistry.register(new Material("lava", "Lava", 0xc0392b).setSolid(true));
Material m = MaterialRegistry.get("lava");   // never null; unknown ids return gray

Unknown ids resolve to a neutral gray placeholder, so a level that references a not-yet-registered material still loads. Materials cover terrain cells and `TerrainFeature`s; the sprite/model assets covered above are a separate system.

23.6. Where to get more

  • Reuse the built-in packs. The four starter packs cover platformer, top-down RPG, board/card and a small 3D kit out of the box — start there and only add what you need.

  • Import your own art. Any PNG (or supported sprite image) can be imported through the editor’s Import action and becomes a first-class asset in your project’s Custom pack. Authoring at the asset’s natural pixel size keeps it crisp.

  • Author packs as JSON. For larger sets, hand-author or generate AssetPack JSON entries alongside the built-ins so the whole pack loads through AssetCatalog.load.

  • Use external game-art sources. Plenty of open game-art libraries and your own design tools (vector editors, pixel-art tools) can produce sprite sheets and tiles. Whichever source you use, you are responsible for honoring its license — check the terms for attribution and commercial-use requirements before shipping art you didn’t create.

Data, media and networking

24. File system, storage, network, and parsing

24.1. JAR resources

Resources packaged within the "JAR" don’t belong in this section of the developer guide, but they’re often confused with Storage and FileSystemStorage, so this is a good place to clarify them.

You can place arbitrary files within the src directory of a Codename One project. The file is packaged into the final distribution. In standard Java SE you can do something like:

InputStream i = getClass().getResourceAsStream("/myFile");

This isn’t guaranteed to work on all platforms and will probably fail on some. Instead, use something like:

InputStream i = Display.getInstance().getResourceAsStream(getClass(), "/myFile");

This isn’t the limitation. You can’t use hierarchies, so something like this fails:

InputStream i = Display.getInstance().getResourceAsStream(getClass(), "/res/myFile");

You can’t use relative paths either, so this also fails (notice the missing first slash):

InputStream i = Display.getInstance().getResourceAsStream(getClass(), "myFile");

Those limitations exist for portability. iOS and Android handle resources differently, so supporting the full Java SE semantics is unrealistic.

This is even worse sometimes. Because of the way iOS works with resources, some file names can fail. For example, names with the @ character or more than one . can cause problems (for example, file.ext.act).

Just like in Java SE, the entries within the "JAR" are read and can’t be modified. You can access the stream, but not the actual file.

24.2. Storage

Codename One gives you four places to keep data, and picking the wrong one is the usual source of trouble later:

The four Codename One data stores and what each is for
Figure 230. What each store is for, and what it isn’t

Storage is accessed through the Storage class. It’s a flat, filesystem-like interface that can list, delete, and write named storage entries.

The Storage API also provides convenient methods to write objects to Storage and read them back with readObject and writeObject.

Objects in Storage are deleted when an app is uninstalled, but they’re retained between application updates. A notable exception is Android, which on some devices keeps objects in Storage after uninstalling an app. To force Android to remove them on uninstall, use the build hint android.allowBackup=false.

The sample code below demonstrates listing storage content, adding and viewing entries, and deleting entries:

Toolbar.setGlobalToolbar(true);
Form hi = new Form("Storage", new BoxLayout(BoxLayout.Y_AXIS));
hi.getToolbar().addCommandToRightBar("+", null, e -> {
    TextField tf = new TextField("", "File Name", 20, TextField.ANY);
    TextArea body = new TextArea(5, 20);
    body.setHint("File Body");
    Command ok = new Command("OK");
    Command cancel = new Command("Cancel");
    Command result = Dialog.show("File Name", BorderLayout.north(tf).add(BorderLayout.CENTER, body), ok, cancel);
    if(ok == result) {
        String name = tf.getText();
        boolean isNew = !Arrays.asList(Storage.getInstance().listEntries()).contains(name);
        try(OutputStream os = Storage.getInstance().createOutputStream(name);) {
            os.write(body.getText().getBytes("UTF-8"));
        } catch(IOException err) {
            Log.e(err);
            return;
        }
        // entrySize() is only right once the stream is closed, and an
        // overwrite already has a row whose size label is now stale
        if(isNew) {
            createFileEntry(hi, name);
        } else {
            hi.removeAll();
            for(String file : Storage.getInstance().listEntries()) {
                createFileEntry(hi, file);
            }
        }
        hi.getContentPane().animateLayout(250);
    }
});
for(String file : Storage.getInstance().listEntries()) {
    createFileEntry(hi, file);
}
hi.show();
private void createFileEntry(Form hi, String file) {
   Label fileField = new Label(file);
   Button delete = new Button();
   Button view = new Button();
   FontImage.setMaterialIcon(delete, FontImage.MATERIAL_DELETE);
   FontImage.setMaterialIcon(view, FontImage.MATERIAL_OPEN_IN_NEW);
   Container content = BorderLayout.center(fileField);
   int size = Storage.getInstance().entrySize(file);
   content.add(BorderLayout.EAST, BoxLayout.encloseX(new Label(size + "bytes"), delete, view));
   delete.addActionListener(e -> {
       Storage.getInstance().deleteStorageFile(file);
       content.setY(hi.getWidth());
       hi.getContentPane().animateUnlayoutAndWait(150, 255);
       hi.removeComponent(content);
       hi.getContentPane().animateLayout(150);
   });
   view.addActionListener(e -> {
       try(InputStream is = Storage.getInstance().createInputStream(file);) {
           String s = Util.readToString(is, "UTF-8");
           Dialog.show(file, s, "OK", null);
       } catch(IOException err) {
           Log.e(err);
       }
   });
   hi.add(content);
}
List of files within the storage
Figure 231. List of files within the storage
Content of a file added to the storage
Figure 232. Content of a file added to the storage

24.2.1. The preferences API

Storage also offers a simple API in the form of the Preferences class. The Preferences class lets developers store simple variables, strings, numbers, booleans, and similar values without writing storage code. This is a common use case in applications. For example, you might need to store a server token:

Preferences.set("token", myToken);

You can then read the token like this:

String token = Preferences.get("token", null);

The backing store filename defaults to "codenameone.properties", but you can override it by calling Preferences.setPreferencesLocation(String) before your app touches the API. This is useful when you need separate preference namespaces per user or when you want to plug in an encrypted storage layer.

When you have a batch of updates, prefer the Preferences.set(Map<String,Object>) overload so that all values are persisted with a single disk write.

Preferences can also tell interested code when a key changes. Register a PreferenceListener with addPreferenceListener() to react to updates made in other parts of your code or triggered remotely through Codename One Push. Remember to remove listeners you no longer need to avoid leaks.

This gets somewhat confusing with primitive numbers for example: if you use Preferences.set("primitiveLongValue", myLongNumber) then invoke Preferences.get("primitiveLongValue", 0) you might get an exception!
This would happen because the value is physically a Long object but you’re trying to get an Integer. The workaround is to remain consistent and use code like this Preferences.get("primitiveLongValue", (long)0).

24.3. File system

FileSystemStorage provides file system access. It maps to the underlying OS’s file system API providing most of the common operations expected from a file API somewhat in the vain of java.io.File & java.io.FileInputStream for example: opening, renaming, deleting etc.

Notice that the file system API is somewhat platform specific in its behavior. All paths used the API should be absolute otherwise they aren’t guaranteed to work.

The main reason java.io.File & java.io.FileInputStream weren’t supported directly has a lot to do with the richness of those two APIs. They effectively allow saving a file anywhere, but mobile devices are far more restrictive and don’t allow apps to see/change files that are owned by other apps.

24.3.1. File paths & app home

All paths in FileSystemStorage are absolute, this simplifies the issue of portability since the concept of relativity and current working directory aren’t portable.

All URLs use the / as their path separator you try to enforce this behavior even on Windows.

Directories end with the / character and thus can be distinguished by their name.

The FileSystemStorage API provides a getRoots() call to list the root directories of the file system (you can then "dig in" through the listFiles API). For example, this is confusing and unintuitive for developers.

To simplify the process of creating/reading files Codename One provides the getAppHomePath() method. This method allows you to get the path to a directory where files can be stored/read.

You can use this directory to place an image to share as you did in the share sample.

A common Android hack is to write files to the SDCard storage to share them among apps. Android 4.x disabled the ability to write to arbitrary directories on the SDCard even when the appropriate permission was requested.

24.3.2. Native file chooser

Use Display.openFileChooser() or the shorter CN.openFileChooser() when the user needs to select a general document such as a PDF, text file, private key, or import file. This is different from Display.openGallery(), which is intended for photo and video library media.

The callback source is a String path that can be read with FileSystemStorage.openInputStream(). If the user cancels the picker the event source is null.

CN.openFileChooser(evt -> {
    if(evt == null || evt.getSource() == null) {
        return;
    }
    String path = (String)evt.getSource();
    try(InputStream in = FileSystemStorage.getInstance().openInputStream(path)) {
        String text = Util.readToString(in, "UTF-8");
        // import the selected file
    } catch(IOException err) {
        Log.e(err);
    }
}, "pdf,txt");

The second argument is a comma-separated accept list. It can contain filename extensions such as pdf,txt,p8, MIME types such as application/pdf, or null// for any file. JavaSE uses the native AWT file dialog. Android uses the system document picker and copies content URIs into an app-readable file path when needed. Ports without a native document picker fall back to a Codename One file tree.

No photo-library or media permissions are added by this API. On Android the system document picker grants read access to the selected document. On iOS this API should be implemented with a document picker, not the photo library; ios.NSPhotoLibraryUsageDescription is only needed for openGallery().

A more advanced usage of the FileSystemStorage API can be a FileSystemStorage Tree:

Form hi = new Form("FileSystemTree", new BorderLayout());
TreeModel tm = new TreeModel() {
    @Override
    public Vector getChildren(Object parent) {
        String[] files;
        if(parent == null) {
            files = FileSystemStorage.getInstance().getRoots();
            return new Vector<Object>(Arrays.asList(files));
        } else {
            try {
                files = FileSystemStorage.getInstance().listFiles((String)parent);
            } catch(IOException err) {
                Log.e(err);
                files = null;
            }
            if(files == null) {
                // the directory went away or cannot be read
                files = new String[0];
            }
        }
        String p = (String)parent;
        Vector result = new Vector();
        for(String s : files) {
            result.add(p + FileSystemStorage.getInstance().getFileSystemSeparator() + s);
        }
        return result;
    }

    @Override
    public boolean isLeaf(Object node) {
        return !FileSystemStorage.getInstance().isDirectory((String)node);
    }
};
Tree t = new Tree(tm) {
    @Override
    protected String childToDisplayLabel(Object child) {
        String n = (String)child;
        // the paths above are built with the platform separator, which is
        // a backslash on Windows
        char sep = FileSystemStorage.getInstance().getFileSystemSeparator();
        // roots arrive with a trailing separator
        while(n.length() > 1 && n.charAt(n.length() - 1) == sep) {
            n = n.substring(0, n.length() - 1);
        }
        int pos = n.lastIndexOf(sep);
        if(pos < 0 || pos == n.length() - 1) {
            return n; // a bare root has no basename to show
        }
        return n.substring(pos + 1);
    }
};
hi.add(BorderLayout.CENTER, t);
hi.show();
Simple sample of a tree for the FileSystemStorage API
Figure 233. Simple sample of a tree for the FileSystemStorage API

24.3.3. Storage vs. file system

The question of storage vs. file system is often confusing for novice mobile developers. This embeds two separate questions:

  • Why are there 2 APIs where one would have worked?

  • Which one should you pick?

The main reasons for the 2 APIs are technical. Many OSes provide 2 ways of accessing data specific to the app and this is reflected within the API. For example: on Android the FileSystemStorage maps to APIs such as java.io.FileInputStream whereas the Storage maps to Context.openFileInput().

The secondary reason for the two APIs is conceptual. FileSystemStorage is more powerful and in a sense provides more ways to fail, this is compounded by the complex on-device behavior of the API. Storage is designed to be friendlier to the uninitiated and more portable.

You should pick Storage unless you have a specific need that prevents it. Some APIs such as Capture expect a FileSystemStorage URI so in those cases this would also be a need.

Another case where FileSystemStorage is beneficial is the case of hierarchy or native API usage. If you need a directory structure or need to communicate with a native API the FileSystemStorage approach is easier.

In some OSes the FileSystemStorage API can find the content of the Storage API. As one is implemented on top of the other. This is undocumented behavior that can change at any moment!

To summarize the differences between the 3 file storage options:

Table 11. Compare Storage, FileSystem & Jar Resources
OptionStorageFile SystemJAR Resource

Main Use Case

General application Data

low-level access

Ship data within the app

Initial State

Blank

Blank

As defined by developer

Modifiable

Yes

Yes

No

Supports Hierarchies

No

Yes

No

24.4. SQL

SQLite is a small SQL database designed for embedding into an application. Codename One exposes it through com.codename1.db, an API similar in spirit to JDBC but much smaller, since the abstractions JDBC needs for pluggable drivers make no sense for a local file.

Use it when you have tabular data, need to query it, or have enough of it that loading the lot into memory is unreasonable. For a handful of values Storage is simpler.

Portability of SQLite

The database API behaves the same on every platform Codename One targets: Android, iOS, the native Windows and Linux builds, JavaScript and the simulator. One contract, documented in the com.codename1.db package documentation, and a single test suite that every port has to pass.

This wasn’t always the case, and it’s worth knowing what changed if you have existing code. Cursors used to count rows from zero on some platforms and from one on others; first() reported success on an empty result set on iOS; the simulator couldn’t seek at all; execute() ran only the first statement of a script on some ports without saying so; and blobs couldn’t be read on iOS. Every one of those is now uniform. If your application depended on the old behaviour, see Section 24.4.7, “Compatibility with the previous behaviour”.

Two platforms don’t compile their own SQLite. Android uses the engine in the operating system, so its version follows the OS. Everything else runs a single pinned engine. The file format is stable across SQLite versions regardless, so a database file remains portable either way.

The Database API opens a database by name:

void openDatabase() throws IOException {
    // A plain identifier is the portable form: it creates the file on first
    // use and every platform decides where it lands. Where
    // Database.isCustomPathSupported() answers true a "file://" path works
    // instead; where it does not, a name with a separator in it throws.
    //
    // Its own database, not the propertiesdemo.db the SQLMap walkthrough
    // later in this chapter creates -- SQLMap.createTable will not migrate
    // a Contact table this left behind with fewer columns.
    Database db = Database.openOrCreate("opendemo.db");
    try {
        // A fresh database is empty, so create before reading or writing.
        db.execute("CREATE TABLE IF NOT EXISTS Person ("
                + "id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)");
        db.execute("INSERT INTO Person (name) VALUES (?)", new String[] { "Ada" });
    } finally {
        Util.cleanup(db);
    }
}

Some SQLite apps ship with a "ready made" database. You allow you to replace the DB file by using the code:

String path = Display.getInstance().getDatabasePath("databaseName");

You can then use the FileSystemStorage class to write the content of your DB file into the path. Notice that it must be a valid SQLite file!

In the JavaScript port databases live in a browser storage pool rather than a filesystem, so getDatabasePath() returns the name within that pool and FileSystemStorage can’t open it.
The browser engine changed. Earlier JavaScript builds kept databases in WebSQL, which Chrome removed and Firefox never implemented; the current engine is SQLite compiled to WebAssembly, and it can’t read a WebSQL store. If your application ran on a browser old enough to still have one, openOrCreate refuses to create a database whose name it finds there rather than handing you an empty one, because an application that writes as it goes would overwrite the user’s data with an empty state. Export what the old store holds from a build that still reads it, or set the cn1.db.ignoreLegacyWebSql property to true to continue with a new empty database — nothing removes the old store, so an application that has finished with it needs that property to move on.

This is useful for applications that need to synchronize with a central server or applications that ship with a large database as part of their core product.

Working with a database is pretty trivial, the application logic below can send arbitrary queries to the database and present the results in a Table. You can probably integrate this code into your app as a debugging tool:

Toolbar.setGlobalToolbar(true);
Style s = UIManager.getInstance().getComponentStyle("TitleCommand");
FontImage icon = FontImage.createMaterial(FontImage.MATERIAL_QUERY_BUILDER, s);
Form hi = new Form("SQL Explorer", new BorderLayout());
hi.getToolbar().addCommandToRightBar("", icon, (e) -> {
    TextArea query = new TextArea(3, 80);
    Command ok = new Command("Execute");
    Command cancel = new Command("Cancel");
    if(Dialog.show("Query", query, ok, cancel) == ok) {
        Database db = null;
        Cursor cur = null;
        try {
            db = Display.getInstance().openOrCreate("MyDB.db");
            String sql = query.getText().trim().toLowerCase();
            // Deciding this properly means parsing the statement, which is a
            // long way outside a sample about Database and Cursor. These four
            // prefixes always return rows; a CTE can do either, so it goes to
            // execute() and a WITH that ends in a SELECT will report success
            // without a table.
            boolean returnsRows = sql.startsWith("select") ||
                    sql.startsWith("pragma") || sql.startsWith("explain") ||
                    sql.startsWith("values");
            if(returnsRows) {
                cur = db.executeQuery(query.getText());
                int columns = cur.getColumnCount();
                hi.removeAll();
                if(columns > 0) {
                    boolean next = cur.next();
                    if(next) {
                        ArrayList<String[]> data = new ArrayList<>();
                        String[] columnNames = new String[columns];
                        for(int iter = 0 ; iter < columns ; iter++) {
                            columnNames[iter] = cur.getColumnName(iter);
                        }
                        while(next) {
                            Row currentRow = cur.getRow();
                            String[] currentRowArray = new String[columns];
                            for(int iter = 0 ; iter < columns ; iter++) {
                                currentRowArray[iter] = currentRow.getString(iter);
                            }
                            data.add(currentRowArray);
                            next = cur.next();
                        }
                        Object[][] arr = new Object[data.size()][];
                        data.toArray(arr);
                        hi.add(BorderLayout.CENTER, new Table(new DefaultTableModel(columnNames, arr)));
                    } else {
                        hi.add(BorderLayout.CENTER, "Query returned no results");
                    }
                } else {
                    hi.add(BorderLayout.CENTER, "Query returned no results");
                }
            } else {
                db.execute(query.getText());
                hi.add(BorderLayout.CENTER, "Query completed successfully");
            }
            hi.revalidate();
        } catch(IOException err) {
            Log.e(err);
            hi.removeAll();
            hi.add(BorderLayout.CENTER, "Error: " + err);
            hi.revalidate();
        } finally {
            Util.cleanup(db);
            Util.cleanup(cur);
        }
    }
});
hi.show();
The rows a select returned presented in a Table
Figure 234. The rows a select returned, presented in a Table
Issuing a query
Figure 235. Issuing a query

24.4.1. Encrypting a database

Pass a DatabaseConfig to encrypt a database at rest. Check that the platform can do it first:

if (Database.isEncryptionSupported()) {
    DatabaseConfig config = DatabaseConfig.managed();
    Database db = Database.openOrCreate("secure.db", config);
    config.wipe();
}

There are three ways to key a database.

DatabaseConfig.passphrase(String)

The key is derived from a secret your application supplies, usually one the user typed. Nothing is stored on the device, so losing the passphrase loses the data.

DatabaseConfig.managed()

A random key is generated on first use and kept in the platform key store, the Android Keystore or the iOS keychain. Your application never handles the key. This is the right default when there is nobody to prompt.

DatabaseConfig.rawKey(byte[])

Thirty-two bytes of key material you already have, for instance issued by your server. No key derivation is applied, so the bytes must already be uniformly random.

A passphrase written into your source code isn’t a secret. String literals are recoverable from a shipped .ipa or .apk in minutes, so a constant passphrase buys you nothing against anyone holding the file. If your application can’t ask a human for one, use managed(): a random key in the platform key store beats a constant compiled into the binary.

Encryption protects data at rest, and nothing else. It’s no defense against a rooted or jailbroken device, a debugger attached to the running process, or a memory dump, because the key is in memory for as long as the database is open.

Managed keys aren’t recoverable. Losing the key store entry, through an application uninstall, a device wipe or, on Android, restoring a backup onto a different device, leaves the database permanently unreadable. Android keystore keys can’t be exported; iOS keychain entries do survive an encrypted backup and restore. If the data has to outlive the device, use a passphrase your user or your server holds.

The simulator has no hardware key store, so a managed key there is protected only by a software derived key in your desktop user profile. It’s fine for development, and it’s no evidence that real user data is protected. DatabaseConfig.isKeyHardwareBacked() reports the difference, and the simulator logs a warning the first time you use a managed key.
What encryption costs your build

Nothing is added to an application that doesn’t use it. The build scans your own classes — not the framework’s — and decides from what they reference:

What your code referencesWhat the build adds

nothing in com.codename1.db

Nothing at all. No SQLite engine on the platforms that bundle one, no browser payload.

com.codename1.db, but never DatabaseConfig

The plain SQLite engine where the platform needs one: compiled into the native Windows and Linux binaries, and about 1.5MB of WebAssembly in a JavaScript build. Android and iOS use the engine already in the operating system.

DatabaseConfig

The cipher as well. On Android that’s net.zetetic:sqlcipher-android and androidx.sqlite; on iOS a bundled SQLCipher build that replaces the system libsqlite3.

On Android, encryption raises android.min_sdk_version to 23 and requires android.useAndroidX=true, because that’s SQLCipher’s floor. If your application targets an older API level the build will raise it for you, and devices below API 23 will no longer see your app in the store. This applies as soon as anything in your code mentions DatabaseConfig, so a build hint alone won’t opt out of it — remove the reference if you need the lower floor.
Converting an existing database

An application that already ships a plaintext database can convert it in place:

if (!Database.isEncrypted("myapp.db")) {
    Database.encrypt("myapp.db", DatabaseConfig.managed());
}

Database.decrypt is the reverse, and Database.changeKey re-keys a database that’s already open. An interruption leaves the original file intact, and schema metadata such as PRAGMA user_version survives.

How that’s achieved differs by platform, which matters only if you’re watching the filesystem. Most platforms re-key the file in place. Android can’t: its SQLCipher engine re-keys only between two encrypted states and refuses to convert to or from plaintext, so the port writes the converted database alongside the original and swaps it in once it’s complete. A .cn1migrate file next to your database means a conversion was interrupted partway; the original is still the live one, and the leftover is cleaned up on the next attempt.

The on-disk format

Every platform reads and writes one format, so a database created on a phone can be pulled off the device and opened in the simulator while you are debugging. The parameters are fixed: AES-256 in CBC mode, PBKDF2-HMAC-SHA512 key derivation at 256000 iterations, a 4096 byte page size and per-page HMAC-SHA512. This is the SQLCipher 4 format, so standard tools such as the sqlcipher command line client can open these files too.

24.4.2. Transactions

Transactions are flat, because that’s the only model all the underlying engines can express. A beginTransaction() while one is already open throws, and so does a commitTransaction() or rollbackTransaction() with none open. Both end the transaction and return the connection to autocommit, and closing a database with one open rolls it back.

A commit can fail. SQLite checks a deferred constraint when the transaction commits, not when the offending row is inserted, so that’s where the failure surfaces. When it does, the transaction is over: the port ends it for you and beginTransaction() works again straight away. Don’t roll back afterward, because there’s nothing left to roll back and the call throws. The engines don’t agree here on their own — Android ends the transaction before it reports the failure, while the SQLite C API and JDBC leave it open — so the port reconciles them and your code sees one behaviour.

24.4.3. Attaching a second database

ATTACH makes another database part of the connection, and SQLite holds it until you detach it or close the connection. Codename One counts an attached database as open, so deleting it, converting it or changing its key is refused while it’s attached, the same as for the database you opened directly. That’s the protection you want: unlinking a file SQLite still has open loses everything written through it.

SQLite lets you attach one file under several schema names at once. The file stays protected until the last of them is detached, so a DETACH of one name doesn’t release a database the connection still holds under another.

Write the filename as a single quoted literal, or bind it as a parameter. SQLite also accepts an expression there — ATTACH '/data/' || name AS aux — but nothing can tell which file that names until the engine evaluates it, which is after the database is already attached and too late to refuse. Those are rejected, and the message says so; build the name in your own code and pass it as a parameter.

Run ATTACH and DETACH through execute(). executeQuery() refuses them, because a cursor runs its statement when it’s stepped — so the attachment would happen after the bookkeeping, and the file could then be deleted while SQLite still holds it.

A path that runs through a symbolic link identifies the same database as the resolved path does. The engine resolves links when it reports what it opened, so Codename One resolves them too before it decides two names are one file — on the platforms where the engine does. Windows is the exception, because SQLite’s Windows layer collapses . and .. as text and steps over a junction rather than through it, so the unresolved name is what identifies the file there.

Name the file with an absolute path from Database.getDatabasePath(). A relative name is resolved by SQLite against the process working directory, which is a different place on every platform and isn’t something a Codename One application controls, while Codename One would look for it in the database directory — so the database protected wouldn’t be the database attached. Those are rejected on every port whose engine has a working directory to resolve against; in the browser, where a name is an entry in a storage pool and means the same thing either way, they’re accepted. If an attachment turns out to name a database that something else is deleting or converting, it’s detached again and the next call on that connection throws to tell you.

24.4.4. Threading

A Database and its cursors aren’t thread safe. Either give each thread its own connection, or wrap one in ThreadSafeDatabase, which routes every call through a single worker thread:

Database threadSafeDb = new ThreadSafeDatabase(Database.openOrCreate("shared.db"));

The wrapper costs a thread handoff per call, so a connection per thread is faster when the threads don’t genuinely need to share one.

24.4.5. Text and column indexes

Text is stored and read back exactly as written, for every character a Java String can hold: accented characters, scripts outside ASCII, emoji, and even the character with code point zero, which SQLite stores in a TEXT value like any other. A string written on one platform reads back identical on every other.

Reading a column the result set doesn’t have — a negative index, or one past the last column — raises an IOException. It’s never reported as a null value, because SQLite’s own C interface answers an out-of-range index with SQL NULL, and passing that on would make a mistyped index impossible to tell apart from stored data.

24.4.6. Cursor performance

Iterating forward with next() costs the same everywhere. Seeking backwards or to an absolute row is cheap on Android, which keeps a window of rows, and costs time proportional to the distance from the start elsewhere, because a SQLite statement only steps forward and seeking back means rewinding and stepping again. Prefer next() for large result sets.

One consequence is worth knowing: because a backward seek re-runs the statement, a cursor is a repeatable read only inside a transaction. Outside one, another thread can write between the two passes, changing what the second pass sees.

There’s a limit on Android that follows from the same mechanism. A statement that changes the database and returns rows — INSERT …​ RETURNING and its relatives — runs once, and Android reads its rows through a window it can only refill by running the statement again. The rows that fit in the first window are therefore readable and the rest aren’t: reaching them would apply the insert a second time. Codename One refuses that rather than doing it, and says so in the error, including that the changes are already applied and the statement must not be retried. If the result set may be large, run the statement with execute() and query for what you need afterward. Every other port steps through such a statement once and has no such limit.

24.4.7. Compatibility with the previous behaviour

This API predates the portable contract, and its behaviour used to differ between platforms. If your application depends on the old behaviour, set the db.legacy build hint, or call Database.setLegacyBehavior(true) before opening any database. It restores each platform’s previous behaviour exactly.

What your project gets by default

An Ant project that uses com.codename1.db is built in compatibility mode automatically. Ant projects predate the portable contract, so a rebuild shouldn’t change how their queries behave unless you ask for it. A Maven project gets the portable contract, which is the documented default everywhere else in this section.

The build server decides this from the project itself, and says so in the build log when it turns compatibility mode on. It only applies when your application actually references com.codename1.db; an Ant project with no database is unaffected.

The build hint always wins, in either direction:

db.legacyAnt projectMaven project

unset

compatibility mode

portable contract

false

portable contract

portable contract

true

compatibility mode

compatibility mode

An Ant project can therefore opt in to the portable contract by setting db.legacy=false, and a Maven project can pin compatibility mode with db.legacy=true. Setting it explicitly is worth doing once you’ve decided, because it makes the choice visible in the project rather than inferred from how it’s built. If you migrate an Ant project to Maven without setting the hint, the default flips, so set it before you migrate if you want the behaviour to stay put.

The hint works in the simulator too, so you can test either mode without a device.

Restored behaviourPlatforms

first() rewinds without landing on a row and always reports success

iOS

getPosition() counts from one

Simulator

wasNull() reports true before any value has been read

Android, iOS

Parameters bind as text rather than by type

iOS

execute(String) runs only the first statement of a script

Android, Simulator

The parameterized methods discard statements after the first, without saying so

all

A nested beginTransaction() is accepted

Android

Malformed SQL surfaces from next() rather than from executeQuery

Android

rollbackTransaction leaves the connection outside autocommit

Simulator

getColumnName reports the table column rather than the result set label

Simulator

executeQuery accepts a transaction control statement such as BEGIN

all

executeQuery accepts ATTACH and DETACH

all

ATTACH accepts an expression for the filename rather than a literal or a parameter

all

ATTACH accepts a relative filename

all but the browser

The flag doesn’t cover defects, nor capabilities that used to throw and now work: last(), prev() and position(int) on iOS and the simulator, reading blobs on iOS, blob query parameters, binding null inside a String[], the existence of a database at all on the native Windows and Linux ports, or seeking backwards through a statement that writes, which used to run that statement a second time and repeat what it wrote. No application can depend on those.

24.4.8. Two settings the contract can’t reach

Everything above is behaviour Codename One implements, so it can be made the same everywhere. Two things are decided when the SQLite engine itself is compiled, and Codename One only compiles one of those engines — Android uses the platform’s, the simulator uses the JDBC driver’s, and iOS uses Apple’s unless the application encrypts:

  • Double-quoted string literals. SELECT "value" isn’t valid SQL — the double quotes make it an identifier — but SQLite has a compatibility mode that turns an unknown identifier into a string, and that mode is fixed when the engine is compiled, with no pragma to change it. The Android and Apple engines accept it; the simulator’s driver rejects it with a message that tells you to use single quotes. Write string literals in single quotes and the question never arises. The engine Codename One bundles for Windows, Linux and encrypted iOS keeps SQLite’s default, so switching encryption on never changes what a statement means.

  • Foreign keys. SQLite ignores declared foreign keys unless asked, and one driver enables them on its own. Codename One puts every port on SQLite’s default so a violation behaves the same in the simulator as on a device; run PRAGMA foreign_keys = ON after opening if you want them enforced, which works everywhere.

Both are easy to settle for yourself: SELECT "value" either returns the string or reports an error, and PRAGMA foreign_keys reports whether they’re on. Codename One’s own port conformance run reports both for every platform it covers.

24.5. Network manager & connection request

One of the more common problems in Network programming is spawning a new thread to handle the network operations. In Codename One this is done seamlessly and becomes unessential thanks to the NetworkManager.

NetworkManager effectively alleviates the need for managing network threads by managing the complexity of network threading. The connection request class can be used to ease web service requests when coupled with the JSON/XML parsing capabilities.

That convenience comes with one rule worth learning before the API: the callbacks that read and write the request run on a network thread, while postResponse, a response listener and an error listener all run on the EDT.

A network request and which thread each of its callbacks runs on
Figure 236. A request through the queue, and the thread each callback arrives on

To open a connection one needs to use a ConnectionRequest object, which has some similarities to the networking mechanism in JavaScript but is obviously somewhat more elaborate.

You can send a get request to a URL using something like:

ConnectionRequest request = new ConnectionRequest(url, false);
request.addResponseListener((e) -> {
    // process the response
});

// request will be handled asynchronously
NetworkManager.getInstance().addToQueue(request);

Notice that you can also implement the same thing and much more by avoiding the response listener code and instead overriding the methods of the ConnectionRequest class which offers many points to override for example:

ConnectionRequest request = new ConnectionRequest(url, false) {
   protected void readResponse(InputStream input) {
        // just read from the response input stream
   }

   protected void postResponse() {
        // invoked on the EDT after processing is complete to allow the networking code
        // to update the UI
   }

   protected void buildRequestBody(OutputStream os) {
        // writes post data, by default this "just works" but if you want to write this
       // manually then override this
   }
};
NetworkManager.getInstance().addToQueue(request);
Notice that overriding buildRequestBody(OutputStream) will work for POST requests and will replace writing the arguments
You don’t need to close the output/input streams passed to the request methods. They’re implicitly cleaned up.

NetworkManager also supports synchronous requests which work in a similar way to Dialog through the invokeAndBlock call and thus don’t block the EDT [8] illegally. For example: you can do something like this:

ConnectionRequest request = new ConnectionRequest(url, false);
// request will be handled synchronously
NetworkManager.getInstance().addToQueueAndWait(request);
byte[] resultOfRequest = request.getResponseData();

Notice that in this case the addToQueueAndWait method returned after the connection completed. Also notice that this was totally legal to do on the EDT!

24.5.1. Timeouts & retries

Each request can override the global timeout using setTimeout(int), which expects a duration in milliseconds. This is useful when you’re talking to endpoints that occasionally need a longer window than the default NetworkManager setting. You can also configure how many times Codename One should retry a failing call by using setSilentRetryCount(int). When the silent retry limit is reached the standard error handling kicks in (for example: listeners fire, fail dialogs show, etc.).

24.5.2. Threading

By default the NetworkManager launches with a single network thread. This is enough for simple applications that don’t do too much networking but if you need to fetch many images concurrently and perform web services in parallel this might be an issue.

Once you increase the thread count there is no guarantee of order for your requests. Requests might not execute in the order with which you added them to the queue!

To update the number of threads use:

NetworkManager.getInstance().updateThreadCount(4);

The callbacks you override on a ConnectionRequest occur on the network thread and not on the EDT!

The callbacks you register are the other way round. postResponse(), designed to update the UI after the networking code completes, is dispatched back to the EDT with callSerially, and so are the listeners added through addResponseListener and NetworkManager.addErrorListener.

Never change the UI from a ConnectionRequest callback. You can either use a listener on the ConnectionRequest, use postResponse() (which is the exception to this rule) or wrap your UI code with callSerially.

24.5.3. VPN detection (best effort)

NetworkManager includes two APIs for querying VPN state:

NetworkManager nm = NetworkManager.getInstance();
if (nm.isVPNDetectionSupported()) {
    boolean vpnActive = nm.isVPNActive();
}
This is a heuristic signal, not a security control.

Both Android and iOS implementations rely on platform networking metadata and interface-name patterns (for example: tun, ppp, utun, ipsec). This means the result can be wrong in either direction:

  • False negatives: Some VPNs may hide or avoid these patterns.

  • False positives: Other virtual/tunnel interfaces may look like VPNs.

Use this API for UX or diagnostics (for example: warnings, telemetry, or logging), not for access control or fraud prevention by itself.

24.5.4. Arguments, headers & methods

HTTP/S is a complex protocol that expects complex encoded data for its requests. Codename One tries to simplify and abstract most of these complexities behind common sense APIs while still providing the full low-level access you would expect from such an API.

Arguments

HTTP supports many "request methods," most commonly GET & POST but also a few others such as HEAD, PUT, DELETE etc.

Arguments in HTTP are passed differently between GET and POST methods. That’s what the setPost method in Codename One determines, whether arguments added to the request should be placed using the GET semantics or the POST semantics.

If you continue your example from above you can do something like this:

ConnectionRequest request = new ConnectionRequest(url, false);
request.addArgument("MyArgName", value);

This will implicitly add a get argument with the content of value. Notice that you don’t care what value is. It’s implicitly HTTP encoded based on the get/post semantics. In this case it will use the get encoding since you passed false to the constructor.

A simpler implementation could do something like this:

ConnectionRequest request = new ConnectionRequest(url +
    "?MyArgName=" + Util.encodeUrl(value), false);

This would be almost identical but doesn’t provide the convenience for switching back and forth between GET/POST and it isn’t as fluent.

You can skip the encoding in complex cases where server code expects illegal HTTP characters (this happens) using the addArgumentNoEncoding method. You can also add many arguments with the same key using addArgumentArray.

Methods

As you explained above, the setPost() method allows you to manipulate the get/post semantics of a request. This implicitly changes the POST or GET method submitted to the server.

For example, if you wish to have finer grained control over the submission process for example: for making a HEAD request you can do this with code like:

ConnectionRequest request = new ConnectionRequest(url, false);
request.setHttpMethod("HEAD");
Headers

When communicating with HTTP servers you often pass data within headers for authentication/authorization but also to convey various properties.

Some headers are built-in as direct APIs for example: content type is directly exposed within the API since it’s a pretty common use case. You can set the content kind of post request using:

ConnectionRequest request = new ConnectionRequest(url, true);
request.setContentType("text/xml");

You can also add any arbitrary header type you want, for example: a common use case is basic authorization where the authorization header includes the Base64 encoded user/password combination as such:

String authCode = user + ":" + password;
String authHeader = "Basic " + Base64.encodeNoNewline(authCode.getBytes());
request.addRequestHeader("Authorization", authHeader);

This can be tedious to do if you want all requests from your app to use this header. For this use case you can use:

String authCode = user + ":" + password;
String authHeader = "Basic " + Base64.encodeNoNewline(authCode.getBytes());
NetworkManager.getInstance().addDefaultHeader("Authorization", authHeader);
Server headers

Server returned headers are a bit trickier to read. You need to subclass the connection request and override the readHeaders method for example:

ConnectionRequest request = new ConnectionRequest(url, false) {
    protected void readHeaders(Object connection) throws IOException {
        String[] headerNames = getHeaderFieldNames(connection);
        for(String headerName : headerNames) {
            String headerValue = getHeader(connection, headerName);
            //....
        }
    }
    protected void readResponse(InputStream input) {
        // just read from the response input stream
    }
};
NetworkManager.getInstance().addToQueue(request);

Here you can extract the headers one by one to handle complex headers such as cookies, authentication etc.

Error handling

As you noticed above practically all the methods in the ConnectionRequest throw IOException. This allows you to avoid the try/catch semantics and let the error propagate up the chain so it can be handled uniformly by the application.

There are two distinct places where you can handle a networking error:

  • The ConnectionRequest - by overriding callback methods

  • The NetworkManager error handler

Notice that the NetworkManager error handler takes precedence thus allowing you to define a global policy for network error handling by consuming errors.

For example: to block all network errors from showing anything to the user you could do something like this:

NetworkManager.getInstance().addErrorListener((e) -> e.consume());
NetworkManager.getInstance().addToQueue(request);

The error listener is invoked first with the NetworkEvent matching the error. Consuming the event prevents it from propagating further down the chain into the ConnectionRequest callbacks.

You can also override the error callbacks of the various types in the request for example: for a server error code you can do:

ConnectionRequest request = new ConnectionRequest(url, false) {
    protected void handleErrorResponseCode(int code, String message) {
        if(code == 444) {
            // do something
            return;
        }
        super.handleErrorResponseCode(code, message);
    }
    protected void readResponse(InputStream input) {
        // just read from the response input stream
    }
};
NetworkManager.getInstance().addToQueue(request);
The error callback is triggered in the network thread!
As a result it can’t access the UI to show a Dialog or anything like that.

Another approach is to use the setFailSilently(true) method on the ConnectionRequest. This will prevent the ConnectionRequest from displaying any errors to the user. It’s a powerful strategy if you use the synchronous version of the APIs for example:

ConnectionRequest request = new ConnectionRequest(url, false);
request.setFailSilently(true);
NetworkManager.getInstance().addToQueueAndWait(request);
if(request.getResponseCode() < 200 || request.getResponseCode() > 299) {
    // probably an error...
}
This code will work with the synchronous "AndWait" version of the method since the response code will take a while to return for the non-wait version.
Error stream

When you get an error code that isn’t 200/300 you ignore the result. This is problematic as the result might contain information you need. For example: many webservices provide further XML/JSON based details describing the reason for the error code.

Calling setReadResponseForErrors(true) will trigger a mode where even errors will receive the readResponse callback with the error stream. This also means that APIs like getData and the listener APIs will also work in case of error.

24.5.5. GZIP

Gzip is a common compression format based on the lz algorithm, it’s used by web servers around the world to compress data.

Codename One supports GZIPInputStream and GZIPOutputStream, which allow you to compress data seamlessly into a stream and extract compressed data from a stream. This is useful and can be applied to every arbitrary stream.

Codename One also features a GZConnectionRequest, which will automatically unzip an HTTP response if it’s indeed gzipped. Notice that some devices (iOS) always request gzip’ed data and always decompress it for you, but for iOS it doesn’t remove the gziped header. The GZConnectionRequest is aware of such behaviors so it’s better to use that when connecting to the network (if applicable).

By default GZConnectionRequest doesn’t request gzipped data ( unzips it when its received) but its pretty easy to do so add the HTTP header Accept-Encoding: gzip for example:

GZConnectionRequest con = new GZConnectionRequest();
con.addRequestHeader("Accept-Encoding", "gzip");

Do the rest as usual and you should have smaller responses from the servers.

24.5.6. File upload

MultipartRequest tries to simplify the process of uploading a file from the local device to a remote server.

You can always submit data in the buildRequestBody but this is flaky and has some limitations in devices/size allowed. HTTP standardized file upload capabilities through the multipart request protocol, this is implemented by countless servers and is well documented. Codename One supports this out of the box:

MultipartRequest request = new MultipartRequest();
request.setUrl(url);
request.addData("myFileName", fullPathToFile, "text/plain");
NetworkManager.getInstance().addToQueue(request);
MultipartRequest is a ConnectionRequest most stuff you expect from there should work. Even addArgument etc.

Since you assume most developers reading this will be familiar with Java here is the way to implement the multipart upload in the servlet API:

This is server-side Java rather than Codename One, so it isn’t one of the compiled examples:

@WebServlet(name = "UploadServlet", urlPatterns = {"/upload"})
@MultipartConfig(fileSizeThreshold = 1024 * 1024 * 100,
        maxFileSize = 1024 * 1024 * 150,
        maxRequestSize = 1024 * 1024 * 200)
public class UploadServlet extends HttpServlet {
    @Override
    public void doPost(HttpServletRequest req, HttpServletResponse res)
            throws ServletException, IOException {
        // By name, matching the addData(...) call above: getParts() also
        // carries any addArgument values and comes off an unordered map,
        // so the first part is not necessarily the file.
        Part data = req.getPart("myFileName");
        try (InputStream in = data.getInputStream()) {
            // Persist the bytes wherever the upload belongs.
        }
    }
}

24.5.7. Parsing

Codename One has many built in parsers for JSON, XML, CSV & Properties formats. You can use those parsers to read data from the Internet or data that’s shipping with your product. For example: use the CSV data to setup default values for your application.

All your parsers are designed with simplicity and small distribution size; they don’t check and will fail in odd ways when faced with broken data. The main logic behind this is that validation takes up CPU time on the device where CPU is a precious resource.

Parsing CSV

CSV is probably the easiest to use, the "Comma Separated Values" format is a list of values separated by commas (or some other character) with new lines to show another row in the table. These map well to an Excel spreadsheet or database table and are supported by default in all spreadsheets.

To parse a CSV use the CSVParser class as such:

Form hi = new Form("CSV Parsing", new BorderLayout());
CSVParser parser = new CSVParser();
try(Reader r = new CharArrayReader("1997,Ford,E350,\"Super, \"\"luxurious\"\" truck\"".toCharArray());) {
String[][] data = parser.parse(r);
String[] columnNames = new String[data[0].length];
for(int iter=  0 ; iter < columnNames.length ; iter++) {
columnNames[iter] = "Col " + (iter + 1);
}
TableModel tm = new DefaultTableModel(columnNames, data);
hi.add(BorderLayout.CENTER, new Table(tm));
} catch(IOException err) {
Log.e(err);
}
hi.show();
CSV parsing results notice the escaped parentheses and comma
Figure 237. CSV parsing results, notice the escaped parentheses and comma

The data contains a two-dimensional array of the CSV content. You can change the delimiter character by using the CSVParser constructor that accepts a character.

Notice that you used CharArrayReader from the com.codename1.io package for this sample. You would want to use java.util.InputStreamReader for real world data.
JSON

The JSON ("JavaScript Object Notation") format is popular on the web for passing values to/from webservices since it works so well with JavaScript. Parsing JSON is as easy but has two different variations. You can use the JSONParser class to build a tree of the JSON data as such:

JSONParser parser = new JSONParser();
Hashtable response = parser.parse(reader);

The response is a Map containing a nested hierarchy of Collection (java.util.List), Strings and numbers to represent the content of the submitted JSON. To extract the data from a specific path iterate the Map keys and recurs into it.

The sample below uses results from an API of ice and fire that queries structured data about the "Song Of Ice & Fire" book series. Here is a sample result returned from the API for the query https://www.anapioficeandfire.com/api/characters?page=5&pageSize=3 :

[
  {
    "url": "http://www.anapioficeandfire.com/api/characters/13",
    "name": "Chayle",
    "culture": "",
    "born": "",
    "died": "In 299 AC, at Winterfell",
    "titles": [
      "Septon"
    ],
    "aliases": [],
    "father": "",
    "mother": "",
    "spouse": "",
    "allegiances": [],
    "books": [
      "http://www.anapioficeandfire.com/api/books/1",
      "http://www.anapioficeandfire.com/api/books/2",
      "http://www.anapioficeandfire.com/api/books/3"
    ],
    "povBooks": [],
    "tvSeries": [],
    "playedBy": []
  },
  {
    "url": "http://www.anapioficeandfire.com/api/characters/14",
    "name": "Gillam",
    "culture": "",
    "born": "",
    "died": "",
    "titles": [
      "Brother"
    ],
    "aliases": [],
    "father": "",
    "mother": "",
    "spouse": "",
    "allegiances": [],
    "books": [
      "http://www.anapioficeandfire.com/api/books/5"
    ],
    "povBooks": [],
    "tvSeries": [],
    "playedBy": []
  },
  {
    "url": "http://www.anapioficeandfire.com/api/characters/15",
    "name": "High Septon",
    "culture": "",
    "born": "",
    "died": "",
    "titles": [
      "High Septon",
      "His High Holiness",
      "Father of the Faithful",
      "Voice of the Seven on Earth"
    ],
    "aliases": [
      "The High Sparrow"
    ],
    "father": "",
    "mother": "",
    "spouse": "",
    "allegiances": [],
    "books": [
      "http://www.anapioficeandfire.com/api/books/5",
      "http://www.anapioficeandfire.com/api/books/8"
    ],
    "povBooks": [],
    "tvSeries": [
      "Season 5"
    ],
    "playedBy": [
      "Jonathan Pryce"
    ]
  }
]

You will place that into a file named "anapioficeandfire.json" in the src directory to make the next sample simpler:

Form hi = new Form("JSON Parsing", new BoxLayout(BoxLayout.Y_AXIS));
JSONParser json = new JSONParser();
try (Reader r = new InputStreamReader(
        Display.getInstance().getResourceAsStream(getClass(), "/anapioficeandfire.json"), "UTF-8")) {
    Map<String, Object> data = json.parseJSON(r);
    java.util.List<Map<String, Object>> content =
            (java.util.List<Map<String, Object>>) data.get("root"); (1)
    for (Map<String, Object> obj : content) { (2)
        String url = (String) obj.get("url");
        String name = (String) obj.get("name");
        java.util.List<String> titles = (java.util.List<String>) obj.get("titles"); (3)
        if (name == null || name.length() == 0) {
            java.util.List<String> aliases = (java.util.List<String>) obj.get("aliases");
            if (aliases != null && aliases.size() > 0) {
                name = aliases.get(0);
            }
        }
        MultiButton mb = new MultiButton(name);
        if (titles != null && titles.size() > 0) {
            mb.setTextLine2(titles.get(0));
        }
        mb.addActionListener((e) -> Display.getInstance().execute(url));
        hi.add(mb);
    }
} catch (IOException err) {
    Log.e(err);
}
hi.show();
1The JSONParser returns a Map which is great if the root object is a Map but sometimes its a list of elements (as is the case above). In this case a special case "root" element is created to contain the actual list of elements.
2This relies on the entries all being maps, which might not be the case for every API type.
3Notice that the "titles" and "aliases" entries are both lists of elements. You use java.util.List to avoid a clash with com.codename1.ui.List.
Parsed JSON result, clicking the elements opens the URL from the JSON
Figure 238. Parsed JSON result, clicking the elements opens the URL from the JSON
The structure of the returned map is sometimes unintuitive when looking at the raw JSON. The easiest thing to do is set a breakpoint on the method and use the inspect variables capability of your IDE to inspect the returned element hierarchy while writing the code to extract that data

An alternative approach is to use the static data parse() method of the JSONParser class and implement a callback parser for example:

JSONParser.parse(reader, callback);

Notice that a static version of the method is used! The callback object is an instance of the JSONParseCallback interface, which includes many methods. These methods are invoked by the parser to show internal parser states, this is like the way traditional XML SAX event parsers work.

Writing JSON — JSONWriter

For typed DTO serialization use the @Mapped annotation framework (see JSON / XML Object Mapping) plus Mappers.toJson(…​). For ad-hoc maps / lists where a build-time Mapper would be overkill, the com.codename1.io.JSONWriter class is the complement of JSONParser:

// One-shot encoding of any Map / List / String / Number / Boolean / null tree
Map<String, Object> tree = new HashMap<>();
tree.put("name", "ada");
tree.put("values", Arrays.asList(1, 2, 3));
String json = JSONWriter.toJson(tree);

// Streaming variants for large outputs (UTF-8)
JSONWriter.toJson(value, writer);
JSONWriter.toJson(value, outputStream);

For tiny request bodies the fluent builders are usually faster to read than a Map literal:

String body = JSONWriter.object()
        .put("email", email)
        .put("password", password)
        .toJson();

String coords = JSONWriter.array()
        .add(JSONWriter.object().put("lat", 37.7749).put("lng", -122.4194))
        .add(JSONWriter.object().put("lat", 51.5074).put("lng",   -0.1278))
        .toJson();

Strings are double-quoted with the standard backslash escapes for ", \, \n, \r, \t, \b, \f, and control chars below 0x20 are emitted as \u00xx. The writer doesn’t pretty-print; if you need indented output, run the result through an external formatter at debug time.

XML parsing

The XMLParser started its life as an HTML parser built for displaying mobile HTML. That usage has since been deprecated but the parser can still parse many HTML pages and is "loose" in verification. This is both good and bad as the parser will work with invalid data without complaining.

The simplest usage of XMLParser looks a bit like this:

XMLParser parser = new XMLParser();
Element elem = parser.parse(reader);

The Element contains children and attributes. It represents a tag within the XML document and even the root document itself. You can iterate over the XML tree to extract the data from within the XML file.

You’ve had a great sample of working with XMLParser in the Tree Section of this guide.

XMLParser has the complimentary XMLWriter class which can generate XML from the Element hierarchy. This allows developers to mutate (change) the elements and save them to a writer stream.

XPath processing

The Result class provides a subset of XPath, but it isn’t limited to XML documents, it can also work with JSON documents, and even with raw Map objects.

Start by demonstrating how to process a response from the Google Reverse Geocoder API. Start with this XML snippet:

<?xml version="1.0" encoding="UTF-8"?>
<GeocodeResponse>
  <status>OK</status>
  <result> <!-- (irrelevant content removed) -->
      <address_component>
          <long_name>London</long_name>
          <short_name>London</short_name>
          <type>locality</type>
          <type>political</type>
      </address_component>
      <!-- (irrelevant content removed) -->
     <address_component>
          <long_name>Ontario</long_name>
          <short_name>ON</short_name>
          <type>administrative_area_level_1</type>
          <type>political</type>
      </address_component>
      <address_component>
         <long_name>Canada</long_name>
         <short_name>CA</short_name>
         <type>country</type>
         <type>political</type>
      </address_component>
  </result>
</GeocodeResponse>

You want to extract some data above into simpler string results. You can do this using:

Result result = Result.fromContent(input, Result.XML);
String country = result.getAsString("/result/address_component[type='country']/long_name");
String region = result.getAsString("/result/address_component[type='administrative_area_level_1']/long_name");
String city = result.getAsString("/result/address_component[type='locality']/long_name");

If you’re at all familiar with processing responses from webservices, you will notice that what would require many lines of code of selecting and testing nodes in regular java can now be done in a single line using the new path expressions.

In the code above, input can be any of:

  • InputStream directly from ConnectionRequest.readResponse(java.io.InputStream).

  • XML or JSON document in the form of a String

  • XML DOM Element returned from XMLParser

  • JSON DOM Map returned from JSONParser

To use the expression processor when calling a webservice, you could use something like the following to parse JSON (notice this is interchangeable between JSON and XML):

Form hi = new Form("Location", new BoxLayout(BoxLayout.Y_AXIS));
hi.add("Pinpointing Location");
Display.getInstance().callSerially(() -> {
    Location l = Display.getInstance().getLocationManager().getCurrentLocationSync();
    if(l == null) {
        hi.add("Location unavailable");
        hi.revalidate();
        return;
    }
    ConnectionRequest request = new ConnectionRequest("https://maps.googleapis.com/maps/api/geocode/json", false) {
        private String country;
        private String region;
        private String city;
        private String json;

        @Override
        protected void readResponse(InputStream input) throws IOException {
                Result result = Result.fromContent(input, Result.JSON);
                country = result.getAsString("/results/address_components[types='country']/long_name");
                region = result.getAsString("/results/address_components[types='administrative_area_level_1']/long_name");
                city = result.getAsString("/results/address_components[types='locality']/long_name");
                json = result.toString();
        }

        @Override
        protected void postResponse() {
            hi.removeAll();
            hi.add(country);
            hi.add(region);
            hi.add(city);
            hi.add(new SpanLabel(json));
            hi.revalidate();
        }
    };
    request.setContentType("application/json");
    request.addRequestHeader("Accept", "application/json");
    request.addArgument("key", googleApiKey);
    request.addArgument("latlng", l.getLatitude() + "," + l.getLongitude());

    NetworkManager.getInstance().addToQueue(request);
});
hi.show();
/* omitted */

The returned JSON looks something like this (notice it’s snipped because the data is too long):

{
  "status": "OK",
  "results": [
    {
      "place_id": "ChIJJ5T9-iFawokRTPGaOginEO4",
      "formatted_address": "280 Broadway, New York, NY 10007, USA",
      "address_components": [
        {
          "short_name": "280",
          "types": ["street_number"],
          "long_name": "280"
        },
        {
          "short_name": "Broadway",
          "types": ["route"],
          "long_name": "Broadway"
        },
        {
          "short_name": "Lower Manhattan",
          "types": [
            "neighborhood",
            "political"
          ],
          "long_name": "Lower Manhattan"
        },
        {
          "short_name": "Manhattan",
          "types": [
            "sublocality_level_1",
            "sublocality",
            "political"
          ],
          "long_name": "Manhattan"
        },
        {
          "short_name": "New York",
          "types": [
            "locality",
            "political"
          ],
          "long_name": "New York"
        },
        {
          "short_name": "New York County",
          "types": [
            "administrative_area_level_2",
            "political"
          ],
          "long_name": "New York County"
        },
        {
          "short_name": "NY",
          "types": [
            "administrative_area_level_1",
            "political"
          ],
          "long_name": "New York"
        },
        {
          "short_name": "US",
          "types": [
            "country",
            "political"
          ],
          "long_name": "United States"
        },
        {
          "short_name": "10007",
          "types": ["postal_code"],
          "long_name": "10007"
        },
        {
          "short_name": "1868",
          "types": ["postal_code_suffix"],
          "long_name": "1868"
        }
      ],
      "types": ["street_address"],
      "geometry": {
        "viewport": {
          "northeast": {
            "lng": -74.0044642197085,
            "lat": 40.7156470802915
          },
          "southwest": {
            "lng": -74.0071621802915,
            "lat": 40.7129491197085
          }
        },
        "location_type": "ROOFTOP",
        "location": {
          "lng": -74.00581319999999,
          "lat": 40.7142981
        }
      }
    }
    /* SNIPED the rest */
  ]
}
Running the geocode sample above in the simulator
Figure 239. Running the geocode sample above in the simulator

The XML processor handles global selections by using a double slash anywhere within the expression, for example:

// get all address_component names anywhere in the document with a type "political"
String array[] = result.getAsStringArray("//address_component[type='political']/long_name");

// get all types anywhere under the second result (dimension is 0-based)
String[] types = result.getAsStringArray("/result[1]//type");
Notice that Google’s JSON webservice uses plural form for each of the node names in that API (ie. results, address_components, and types) where they don’t in the XML services (that’s result, address_component etc.)
Second example

It’s also possible to do some more complex expressions. You’ll use the following XML fragment for the next batch of examples:

<rankings type="aus" gender="male" date="2011-12-31">
    <player id="1036" coretennisid="6752" rank="1"
       delta="0" singlespoints="485000" doublespoints="675"
       deductedpoints="0" totalpoints="485675">
        <firstname>Bernard</firstname>
        <lastname>Tomic</lastname>
        <town>SOUTHPORT</town>
        <state>QLD</state>
        <dob>1992-10-21</dob>
    </player>
    <player id="2585" coretennisid="1500" rank="2"
       delta="0" singlespoints="313500" doublespoints="12630"
       deductedpoints="0" totalpoints="326130">
        <firstname>Mathew</firstname>
        <lastname>Ebden</lastname>
        <town>CHURCHLANDS</town>
        <state>WA</state>
        <dob>1987-11-26</dob>
    </player>
    <player id="6457" coretennisid="287" rank="3"
        delta="0" singlespoints="132500" doublespoints="1500"
        deductedpoints="0" totalpoints="134000">
       <firstname>Lleyton</firstname>
       <lastname>Hewitt</lastname>
       <town>EXETER</town>
       <state>SA</state>
       <dob>1981-02-24</dob>
    </player>
    <!-- ... etc ... -->
</rankings>

Above, if you want to select the IDs of all players that are ranked in the top 2, you can use an expression like:

int top2[] = result.getAsIntegerArray("//player[@rank < 3]/@id");
Notice above that the expression is using an attribute for selecting both rank and id. In JSON documents, if you try to select an attribute, it will look for a child node under the attribute name you ask for.

If a document is ordered, you might want to select nodes by their position, for example:

String first2[] = result.getAsStringArray("//player[position() < 2]/firstname");

String secondLast = result.getAsString("//player[last() - 1]/firstname");

It’s also possible to select parent nodes, by using the .. expression. For example:

int id = result.getAsInteger("//lastname[text()='Hewitt']/../@id");

Above, you globally find a lastname element with a value of ‘Hewitt’, then grab the parent node of lastname which happens to be the player node, then grab the ID attribute from the player node. Or, you could get the same result from the following simpler statement:

int id = result.getAsInteger("//player[lastname='Hewitt']/@id");

A predicate can’t hold another predicate. An expression such as //player[//address[country/isocode='CA']]/@id is rejected with Syntax error: unclosed predicate, because the outer predicate is split at its comparator before it’s tokenized and that cuts the inner one in half. Select the inner nodes first and match on what comes back.

To select a node based on the existence of an attribute:

int id[] = result.getAsIntegerArray("//player[@rank]/@id");

Above, you selected the IDs of all ranked players. Conversely, you can select the non-ranked players like this:

int id[] = result.getAsIntegerArray("//player[@rank=null]/@id");
Logical not (!) operators aren’t implemented.

There’s no matching test for a child node. A predicate that names one with no comparison in it — //player[middlename] — is rejected with Could not create a comparator, so test for a child by comparing it to something, or select the child and work from what comes back.

Keep in mind that the Codename One path expression language isn’t a full implementation of XPath 1.0, but does already handle many of the most useful features of the specification.

Properties files

Properties files are standard key/value pairs encoded into a text file. This file format is familiar to Java developers and the Codename One specific version tries to be as close as possible to the original Java implementation.

Notice that properties file both in Java proper and in Codename One don’t support non-ascii characters. To encode Unicode values into the properties file format you should use the native2ascii tool that ships with the JDK.

One major difference between standard Java properties and the ones in Codename One is that Codename One sorts properties alphabetically when saving. Java uses random order based on the Hashtable natural ordering.

This was done to provide consistency for saved files.

24.6. Debugging network connections

Debugging Network Connections
Figure 240. Debugging Network Connections

Codename One includes a Network Monitor tool which you can access through the simulator menu option. This tool reflects all the requests made through the connection requests and displays them in the left pane. This allows you to track issues in your code/web service and see everything the is "going through the wire."

This is a useful tool for optimizing and for figuring out what’s happening with your server connection logic.

24.6.1. Simpler downloads

A common task is file download to storage or filesystem.

The Util class has simple utility methods:

downloadUrlToFileSystemInBackground, downloadUrlToStorageInBackground, downloadUrlToFile & downloadUrlToStorage.

These all delegate to a feature in ConnectionRequest:

ConnectionRequest.setDestinationStorage(fileName) & ConnectionRequest.setDestinationFile(fileName);

Both of which simplify the whole process of downloading a file.

Downloading images

Codename One has many ways to download an image and the general recommendation is the URLImage. For example, the URLImage assumes that you know the size of the image in advance or that you’re willing to resize it. In that regard it works great for some use cases but not so much for others.

The download methods mentioned above are great alternatives but they’re a bit verbose when working with images and don’t provide fine grained control over the ConnectionRequest for example: making a POST request to get an image.

Adding global headers is another use case but you can use addDefaultHeader to add those.

To make this process simpler there is a set of helper methods in ConnectionRequest that downloads images directly.

These methods complement the Util methods but go a bit further and feature terse syntax for example: you can download a ConnectionRequest to Storage using code like this:

request.setUrl(url);
request.downloadImageToStorage("cached-image", (img) -> theImageIsHereDoSomethingWithIt(img));

This effectively maps the ConnectionRequest directly to a SuccessCallback for further processing.

URLImage caching

URLImage is great. It changed the way you do some things in Codename One.

For example, when it was introduced there was no support for the cache filesystem or for the JavaScript port. The cache filesystem is probably the best place for images of URLImage so supporting that as a target is an obvious choice, but JavaScript seems to work so why would it need a special case?

JavaScript already knows how to download and cache images from the web. URLImage is actually a step back from the things a good browser can do so why not use the native abilities of the browser when you’re running there and fallback to using the cache filesystem if it’s available and as a last resort go to storage…​

That’s what the new method of URLImage does:

public static Image createCachedImage(String imageName, String url,
                                      Image placeholder, int resizeRule);

A few important things you need to notice about this method:

  • It returns Image and not URLImage. This is crucial. Down casting to `URLImage* will work on the simulator but might fail in some platforms (for example: JavaScript) so don’t do that!
    Since this is implemented natively in JavaScript you need a different abstraction for that platform.

  • It doesn’t support image adapters and instead uses a simplified resize rule. Image adapters work on URLImage since you have a lot of control in that class. For example, in the browser your control is limited and so an adapter won’t work.

If you do use this approach it would be far more efficient when running in the JavaScript port and will make better use of caching in most OSes.

24.7. Rest API

The Rest API makes it easy to invoke a RESTful webservice without many of the nuances of ConnectionRequest. You can use it to define the HTTP method and start building based on that. To get a parsed JSON result from a URL you could do:

Map<String, Object> jsonData = Rest.get(myUrl).getAsJsonMap().getResponseData();

For a lot of REST requests this will fail because you need to add an HTTP header indicating that you accept JSON results. You have a special case support for that:

Map<String, Object> jsonData = Rest.get(myUrl).acceptJson().getAsJsonMap().getResponseData();

POST requests use the same builder pattern:

Map<String, Object> jsonData = Rest.post(myUrl).body(bodyValueAsString).getAsJsonMap().getResponseData();

Notice the usage of post and the body builder method. MANY methods exist in the builder class that cover pretty much everything you would expect and then some when it comes to the needs of rest services.

Some highlights that are easy to miss:

  • .priority(byte) lets you change the underlying ConnectionRequest priority when you need certain calls to jump the queue.

  • .cookiesEnabled(boolean) controls whether cookies are persisted for the request when you need stateless behavior.

  • .useBoolean(boolean) and .useLongs(boolean) toggle how the JSON parser materializes number and boolean types inside the resulting Map, which is handy when your back end is strict about data types.

  • .cacheMode(…​) and .postParameters(…​) expose the same knobs as ConnectionRequest, keeping you in the fluent API even for advanced tweaks.

24.7.1. Top-level JSON arrays — fetchAsJsonList

When the endpoint returns a top-level JSON array (for example [{...}, {...}]), use fetchAsJsonList rather than fetchAsJsonMap. The underlying JSONParser wraps top-level arrays under a synthetic "root" key; fetchAsJsonList unwraps that envelope for you so the callback receives the array directly:

Rest.get("https://api.example.com/items")
    .header("Authorization", "Bearer " + token)
    .acceptJson()
    .fetchAsJsonList(response -> {
        List items = response.getResponseData();
        renderItems(items);
    });

There is no separate builder for the ambiguous "object or array" case: when the response shape isn’t known up-front, use fetchAsJsonMap and branch on data.get("root") instanceof List.

24.7.2. Typed responses — fetchAsMapped and fetchAsMappedList

Rest.fetchAsJsonMap returns a generic Map<String, Object> that the caller casts and inspects key by key. Once your DTOs are @Mapped-annotated (see JSON / XML Object Mapping), fetchAsMapped returns the typed object directly:

// Model
@Mapped
public class Pet {
    @JsonProperty("id")        public long id;
    @JsonProperty("name")      public String name;
    @JsonProperty("photoUrls") public List<String> photoUrls;
}
// Call site
Rest.get(baseUrl + "/pet/" + petId)
    .header("Authorization", "Bearer " + token)
    .acceptJson()
    .fetchAsMapped(Pet.class, response -> {
        Pet pet = response.getResponseData();    // already typed -- no Map casts
        renderPet(pet);
    });

For endpoints that return a list of DTOs, use fetchAsMappedList:

Rest.get(baseUrl + "/albums")
    .header("Authorization", "Bearer " + token)
    .acceptJson()
    .fetchAsMappedList(Album.class, response -> {
        List<Album> albums = response.getResponseData();
        renderAlbums(albums);
    });

Both builders fall back to the untyped fetchAsJsonMap / fetchAsJsonList path internally and then route every map element through the build-time mapper that the @Mapped annotation processor generated. If a mapper isn’t registered for the requested class (typical cause: the process-annotations Mojo didn’t run on the class, or the class isn’t @Mapped-annotated), the callback completes with null data — so always inspect response.getResponseCode() rather than response.getResponseData() to differentiate a server error response from the missing-mapper case.

The code in the kitchen sink webservice sample was updated to use this API. The result is shorter and more readable without sacrificing anything.

24.7.3. Rest in practice - twilio

The best way to explain the usage of this API is through a concrete "real world" example. Twilio provides many great telephony-oriented webservices to developers. One of those is an SMS sending webservice which can be useful for things such as "device activation."

To get started you would need to sign up to Twilio and have the following 3 variable values:

String accountSID = "----------------";
String authToken = "---------------";
String fromPhone = "your Twilio phone number here";
You can open a trial Twilio account and it tags all your SMS’s. Notice you would need to use a You based number if you don’t want to pay

You can now send hello world as an SMS to the end user. Once this is in place sending an SMS through REST is a matter of using the Rest API:

Response<Map> result = Rest.post("https://api.twilio.com/2010-04-01/Accounts/" + accountSID + "/Messages.json").
        queryParam("To", destinationPhone).
        queryParam("From", fromPhone).
        queryParam("Body", "Hello World").
        basicAuth(accountSID, authToken).
        getAsJsonMap();

Notice that this is equivalent of this "curl" command:

curl 'https://api.twilio.com/2010-04-01/Accounts/[accountSID]/Messages.json' -X POST \
--data-urlencode 'To=[destinationPhone]' \
--data-urlencode 'From=[fromPhone]' \
--data-urlencode 'Body=Hello World' \
-u [accountSID]:[AuthToken]

That’s pretty cool as the curl command maps almost directly to the Rest API call!

What you do here is actually pretty trivial, you open a connection the api messages URL. You add arguments to the body of the post request and define the basic authentication data.

The result is in JSON form you ignore it since it isn’t that important but it might be useful for error handling. This is a sample response (redacted keys):

{
    "sid": "[sid value]",
    "date_created": "Sat, 09 Sep 2017 19:47:30 +0000",
    "date_updated": "Sat, 09 Sep 2017 19:47:30 +0000",
    "date_sent": null,
    "account_sid": "[sid value]",
    "to": "[to phone number]",
    "from": "[from phone number]",
    "messaging_service_sid": null,
    "body": "Sent from your Twilio trial account - Hello World",
    "status": "queued",
    "num_segments": "1",
    "num_media": "0",
    "direction": "outbound-api",
    "api_version": "2010-04-01",
    "price": null,
    "price_unit": "USD",
    "error_code": null,
    "error_message": null,
    "uri": "/2010-04-01/Accounts/[sid value]/Messages/SMe802d86b9f2246989c7c66e74b2d84ef.json",
    "subresource_uris": {
        "media": "/2010-04-01/Accounts/[sid value]/Messages/[message value]/Media.json"
    }
}

Notice the error message entry which is null meaning there was no error, if there was an error you’d have a message there or an error code that isn’t in the 200-210 range.

This should display an error message to the user if there was a problem sending the SMS:

if(result.getResponseCode() < 200 || result.getResponseCode() > 299) {
    String error = null;
    if(result.getResponseData() != null) {
        error = (String)result.getResponseData().get("error_message");
    }
    if(error == null) {
        error = "Error sending SMS: " + result.getResponseCode();
    }
    ToastBar.showErrorMessage(error);
}

24.8. Webservice wizard

The right-click option this was driven from belonged to the IDE plugin and is gone. The generator itself survives in one place: Ant projects created from the old template still have a webservice-wizard target, which runs com.codename1.ws.wizard.WSWizard out of CodeNameOneBuildClient.jar. A Maven project has no equivalent. com.codename1.io.WebServiceProxyCall is still in the framework either way, so an app with generated proxies keeps working. For new work use Section 24.7, “Rest API”, which needs no code generation and speaks a format every server already talks.

The protocol it implements is a compact binary one. Method arguments were restricted to a small set of types, extended through object externalization, and every generated method came in two forms: one that blocks and throws IOException, and one that takes a callback and reports through it.

24.9. Connection request caching

Caching server data locally is a huge part of the advantage a native app has over a web app. This is non-trivial as it requires a delicate balance if you want to test the server resource for changes.

HTTP provides two ways to do that the ETag and Last-Modified. While both are great they’re non-trivial to use and by no definition seamless.

Connection request has an experimental feature that allows you to set the caching mode to one of 4 states either globally or per connection request:

  • OFF is the default meaning no caching.

  • SMART means all get requests are cached intelligently and caching is "mostly" seamless

  • MANUAL means that the developer handles the actual caching but the system won’t do a request on a resource that’s already "fresh"

  • OFFLINE will fetch data from the cache and won’t try to go to the server. It will generate a 404 error if data isn’t available

You can toggle these in the specific request by using setCacheMode(CachingMode) and set the global default using setDefaultCacheMode(CachingMode).

Caching applies to GET operations, it won’t work for POST or other methods

There are many methods of interest to keep an eye for:

protected InputStream getCachedData() throws IOException;
protected void cacheUnmodified() throws IOException;
public void purgeCache();
public static void purgeCacheDirectory() throws IOException;

24.9.1. getCachedData()

This returns the cached data. This is invoked to implement readResponse(InputStream) when running offline or when you detect that the local cache isn’t stale.

The smart mode implements this and will fetch the right data. For example, the manual mode doesn’t store the data and relies on you to do so. In that case you need to return the data you stored at this point and must implement this method for manual mode.

24.9.2. Cacheunmodified()

This is a callback that’s invoked to show a cache hit, meaning that you already have the data.

The default implementation still tries to call all the pieces for compatibility (for example: readResponse). For example, if this is unnecessary you can override that method with a custom implementation or even a blank implementation to block such a case.

24.9.3. Purgecache & purgeCacheDirectory

These methods are pretty self-explanatory. Notice one caveat though…​

When you download a file or a storage element you don’t cache them and rely on the file/storage element to be present and serve as "cache." When purging you won’t delete a file or storage element you downloaded and thus these might remain.

For example, you do remove the ETag and Last-Modified data so the files might get refreshed the next time around.

24.10. Cached data service

The CachedDataService allows caching data and updating it if the data changed on the server. The download APIs won’t check for update if there is a local cache of the data (for example: URLImage always uses the local copy). This isn’t a bad thing, it’s pretty efficient.

For example, it might be important to update the image if it changed but you still want caching.

The CachedDataService will fetch data if it isn’t cached locally and cache it. When you "refresh" it will send a special HTTP request that will send back the data if it has been updated since the last refresh:

CachedDataService.register();
CachedData d = (CachedData)Storage.getInstance().readObject("LocallyCachedData");

if(d == null) {
  d = new CachedData();
  d.setUrl("https://....");
}
final CachedData data = d;
// check if there is a new version of this on the server
CachedDataService.updateData(data, new ActionListener() {
    public void actionPerformed(ActionEvent ev) {
        // the fresh copy arrives on the event, updateData leaves ours alone
        CachedData fresh = (CachedData)((NetworkEvent)ev).getMetaData();
        // the service fills in the bytes and validators but not the URL
        fresh.setUrl(data.getUrl());
        Storage.getInstance().writeObject("LocallyCachedData", fresh);
    }
});

24.11. Externalizable objects

Codename One provides the Externalizable interface, which is like the Java SE Externalizable interface. This interface allows an object to declare itself as Externalizable for serialization (so an object can be stored in a file/storage or sent over the network). For example, due to the lack of reflection and use of obfuscation these objects must be registered with the Util class.

Codename One doesn’t support the Java SE Serialization API due to the size issues and complexities related to obfuscation.

The major objects that are supported by default in the Codename One Externalizable are: String, Collection, Map, ArrayList, HashMap, Vector, Hashtable, Integer, Double, Float, Byte, Short, Long, Character, Boolean, Object[], byte[], int[], float[], long[], double[].

Externalizing an object such as h below should work fine:

Map<String, Object> h = new HashMap<>();
h.put("Hi","World");
h.put("data", new byte[] {(byte)1});
Storage.getInstance().writeObject("Test", h);

For example, notice that some things aren’t polymorphic for example: if you will externalize a String[] you will get back an Object[] since String arrays aren’t detected by the implementation.

The externalization process caches objects so the app will seem to work and fail on restart!

Implementing the Externalizable interface is important when you want to store a proprietary object. In this case you must register the object with the com.codename1.io.Util class so the externalization algorithm will be able to recognize it by name by invoking:

Util.register("MyClass", MyClass.class);
You should do this early on in the app for example: in the init(Object) but you shouldn’t do it in a static initializer within the object as that might never be invoked!

An Externalizable object must have a default public constructor and must implement the following 4 methods:

public int getVersion();
public void externalize(DataOutputStream out) throws IOException;
public void internalize(int version, DataInputStream in) throws IOException;
public String getObjectId();

The getVersion() method returns the current version of the object allowing the stored data to change its structure in the future (the version is then passed when internalizing the object). The object ID is a String uniquely representing the object; it corresponds to the class name (in the example above the Unique Name should be MyClass).

It’s a common mistake to use getClass().getName() to implement getObjectId() and it would seem to work in the simulator. This isn’t the case though!
Since devices obfuscate the class names this becomes a problem as data is stored in a random name that changes with every release.

Developers need to write the data of the object in the externalize method using the methods in the data output stream and read the data of the object in the internalize method for example:

public void externalize(DataOutputStream out) throws IOException {
    out.writeUTF(name);
    if(value != null) {
        out.writeBoolean(true);
        out.writeUTF(value);
    } else {
        out.writeBoolean(false);
    }
    if(domain != null) {
        out.writeBoolean(true);
        out.writeUTF(domain);
    } else {
        out.writeBoolean(false);
    }
    out.writeLong(expires);
}

public void internalize(int version, DataInputStream in) throws IOException {
    name = in.readUTF();
    if(in.readBoolean()) {
        value = in.readUTF();
    }
    if(in.readBoolean()) {
        domain = in.readUTF();
    }
    expires = in.readLong();
}

Since strings might be null sometimes you also included convenience methods to implement such externalization. This effectively writes a boolean before writing the UTF to show whether the string is null:

public void externalize(DataOutputStream out) throws IOException {
    Util.writeUTF(name, out);
    Util.writeUTF(value, out);
    Util.writeUTF(domain, out);
    out.writeLong(expires);
}

public void internalize(int version, DataInputStream in) throws IOException {
    name = Util.readUTF(in);
    value = Util.readUTF(in);
    domain = Util.readUTF(in);
    expires = in.readLong();
}

Assuming you added a new date field to the object you can do the following. Notice that a Date is a long value in Java that can be null. For completeness the full class is presented below:

public class MyClass implements Externalizable {
    private static final int VERSION = 2;
    private String name;
    private String value;
    private String domain;
    private Date date;
    private long expires;

    public MyClass() {}

    public int getVersion() {
        return VERSION;
    }

    public String getObjectId() {
        return "MyClass";
    }

    public void externalize(DataOutputStream out) throws IOException {
        Util.writeUTF(name, out);
        Util.writeUTF(value, out);
        Util.writeUTF(domain, out);
        if(date != null) {
            out.writeBoolean(true);
            out.writeLong(date.getTime());
        } else {
            out.writeBoolean(false);
        }
        out.writeLong(expires);
    }

    public void internalize(int version, DataInputStream in) throws IOException {
        name = Util.readUTF(in);
        value = Util.readUTF(in);
        domain = Util.readUTF(in);
        if(version > 1) {
            boolean hasDate = in.readBoolean();
            if(hasDate) {
                date = new Date(in.readLong());
            }
        }
        expires = in.readLong();
    }
}

Notice that you need to check for compatibility during the reading process as the writing process always writes the latest version of the data.

24.12. UI bindings & utilities

Codename One provides many tools to simplify the path between networking/IO & GUI. A common task of showing a wait dialog or progress sign while fetching network data can be simplified by using the InfiniteProgress class for example:

InfiniteProgress ip = new InfiniteProgress();
Dialog dlg = ip.showInifiniteBlocking();
request.setDisposeOnCompletion(dlg);

The process of showing a progress bar for a long IO operation such as downloading is automatically mapped to the IO stream in Codename One using the SliderBridge class.

You can simulate network delays and disconnected network in the Simulator menu

The SliderBridge class can bind a ConnectionRequest to a Slider and effectively show the progress of the download. For example:

Form hi = new Form("Download Progress", new BorderLayout());
Slider progress = new Slider();
Button download = new Button("Download");
download.addActionListener((e) -> {
    ConnectionRequest cr = new ConnectionRequest("https://www.codenameone.com/img/blog/new_icon.png", false);
    SliderBridge.bindProgress(cr, progress);
    NetworkManager.getInstance().addToQueueAndWait(cr);
    if(cr.getResponseCode() == 200) {
        hi.add(BorderLayout.CENTER, new ScaleImageLabel(EncodedImage.create(cr.getResponseData())));
        hi.revalidate();
    }
});
hi.add(BorderLayout.SOUTH, progress).add(BorderLayout.NORTH, download);
hi.show();
SliderBridge progress for downloading the image in the slow network mode
Figure 241. SliderBridge progress for downloading the image in the slow network mode

24.13. Logging & crash protection

Codename One includes a Log API that allows developers to invoke Log.p(String) or Log.e(Throwable) to log information to storage.

As part of the premium cloud features it’s possible to invoke Log.sendLog() to email a log directly to the developer account. Codename One can do that seamlessly based on changes printed into the log or based on exceptions that are uncaught or logged for example:

Log.setReportingLevel(Log.REPORTING_DEBUG);
DefaultCrashReporter.init(true, 2);

This code will send a log every 2 minutes to your email if anything was changed. You can place it within the init(Object) method of your application.

For a production application you can use Log.REPORTING_PRODUCTION which will email the log on exception.

24.14. Sockets

At this moment Codename One supports TCP sockets. Server socket (listen/accept) is available on Android and the simulator but not on iOS.

You can check if Sockets are supported using the Socket.isSupported(). You can test for server socket support using Socket.isServerSocketSupported().

To use sockets you can use the Socket.connect(String host, int port, SocketConnection eventCallback) method.

To listen on sockets you can use the Socket.listen(int port, Class scClass) method which will instantiate a SocketConnection instance (scClass is expected to be a subclass of SocketConnection) for every incoming connection.

This simple example allows you to create a server and a client assuming the device supports both:

public class MyApplication {
    private Form current;

    public void init(Object context) {
        try {
            Resources theme = Resources.openLayered("/theme");
            UIManager.getInstance().setThemeProps(theme.getTheme(theme.getThemeResourceNames()[0]));
        } catch(IOException e){
            e.printStackTrace();
        }
    }

    public void start() {
        if(current != null){
            current.show();
            return;
        }
        final Form soc = new Form("Socket Test");
        Button btn = new Button("Create Server");
        Button connect = new Button("Connect");
        final TextField host = new TextField("127.0.0.1");
        // the handle is the only way to close the listening socket, so a second
        // tap would otherwise leave the first accept thread running for good
        Socket.StopListening[] listening = new Socket.StopListening[1];
        btn.addActionListener((evt) -> {
            if(listening[0] != null) {
                listening[0].stop();
            }
            soc.addComponent(new Label("Listening: " + Socket.getHostOrIP()));
            soc.revalidate();
            listening[0] = Socket.listen(5557, SocketListenerCallback.class);
        });
        connect.addActionListener((evt) -> {
            Socket.connect(host.getText(), 5557, new SocketConnection() {
                @Override
                public void connectionError(int errorCode, String message) {
                    System.out.println("Error");
                }

                @Override
                public void connectionEstablished(InputStream is, OutputStream os) {
                    try {
                        int counter = 1;
                        while(isConnected()) {
                            os.write(("Hi: " + counter).getBytes());
                            counter++;
                            Thread.sleep(2000);
                        }
                    } catch(Exception err) {
                        err.printStackTrace();
                    }
                }
            });
        });
        soc.setLayout(new BoxLayout(BoxLayout.Y_AXIS));
        soc.addComponent(btn);
        soc.addComponent(connect);
        soc.addComponent(host);
        soc.show();
    }

    public static class SocketListenerCallback extends SocketConnection {
        private Label connectionLabel;

        @Override
        public void connectionError(int errorCode, String message) {
            System.out.println("Error");
        }

        private void updateLabel(final String t) {
            Display.getInstance().callSerially(new Runnable() {
                public void run() {
                    if(connectionLabel == null) {
                        connectionLabel = new Label(t);
                        Display.getInstance().getCurrent().addComponent(connectionLabel);
                    } else {
                        connectionLabel.setText(t);
                    }
                    Display.getInstance().getCurrent().revalidate();
                }
            });
        }

        @Override
        public void connectionEstablished(InputStream is, OutputStream os) {
            try {
                byte[] buffer = new byte[8192];
                while(isConnected()) {
                    int size = is.read(buffer, 0, 8192);
                    if(size == -1) {
                        return;
                    }
                    if(size > 0) {
                        updateLabel(new String(buffer, 0, size));
                    }
                }
            } catch(Exception err) {
                err.printStackTrace();
            }
        }
    }

    public void stop() {
        current = Display.getInstance().getCurrent();
    }

    public void destroy() {
    }
}

24.15. Properties

In standard Java you have a POJO (Plain Old Java Object) which has getters/setters for example: you can have a simple Meeting class like this:

public class Meeting {
     private Date when;
     private String subject;
     private int attendance;

     public Date getWhen() {
        return when;
    }

     public String getSubject() {
        return subject;
     }

     public int getAttendance() {
        return attendance;
     }

     public void setWhen(Date when) {
        this.when = when;
    }

     public void setSubject(String subject) {
        this.subject  = subject;
     }

     public void setAttendance(int attendance) {
        this.attendance = attendance;
     }
}

That’s a classic POJO and it’s the force that underlies JavaBeans and a few tools in Java.

The properties are effectively the getters/setters for example: subject, when etc. But properties have many features that are crucial:

  • They can be manipulated in runtime by a tool that had no knowledge of them during compile time

  • They’re observable - a tool can monitor changes to a value of a property

  • They can have meta-data associated with them

These features are crucial since properties allow you all kinds of magic for example: hibernate/ORM uses properties to bind Java objects to a database representation, jaxb does it to parse XML directly into Java objects and GUI builders use them to customize UI’s visually.

POJO’s don’t support most of that so pretty much all Java based tools use a lot of reflection & bytecode manipulation. This works but has a lot of downsides for example: say you want to map an object both to the Database and to XML/JSON.

Would the bytecode manipulation collide?

Would it result in duplicate efforts?

And how do you write custom generic code that uses such abilities? Do you need to manipulate the VM?

24.15.1. Properties in Java

These are all abstract ideas, lets look at how you think properties should look in Java and how you can gain from this moving forward.

This is the same class as the one above written with properties:

public class Meeting implements PropertyBusinessObject {
     public final Property<Date,Meeting> when = new Property<>("when");
     public final Property<String,Meeting> subject = new Property<>("subject");
     public final Property<Integer,Meeting>  attendance = new Property<>("attendance");
     private final PropertyIndex idx = new PropertyIndex(this, "Meeting", when, subject, attendance);

    @Override
    public PropertyIndex getPropertyIndex() {
        return idx;
    }
}

This looks a bit like a handful, so start with usage which might clarify a few things, then dig into the class itself.

With a plain object you would have written this:

Meeting meet = new Meeting();
meet.setSubject("My Subject");
Log.p(meet.getSubject());

With properties you do this:

Meeting meet = new Meeting();
meet.subject.set("My Subject");
Log.p(meet.subject.get());
Encapsulation

At first glance it looks like you created public fields (which you did) but if you will look at the declaration you will notice the final keyword:

public final Property<String,Meeting> subject = new Property<>("subject");

That final is what makes the field safe to expose: meet.subject = otherValue doesn’t compile, because there’s nothing to assign to.

All setting/getting must happen through the set/get methods and they can be replaced. For example: this is valid syntax that prevents setting the property to null and defaults it to an empty string:

public final Property<String,Meeting> subject = new Property<String, Meeting>("subject", "") {
     public Meeting set(String value) {
         if(value == null) {
            return Meeting.this;
         }
         return super.set(value);
     }
};
You will discuss the reason for returning the Meeting instance below
Introspection & observability

Since Property is a common class it’s pretty easy for introspective code to manipulate properties. For example, it can’t detect properties in an object without reflection.

That’s why you have the index object and the PropertyBusinessObject interface (which defines getPropertyIndex).

The PropertyIndex class provides metadata for the surrounding class including the list of the properties within. It allows enumerating the properties and iterating over them making them accessible to all tools.

Furthermore all properties are observable with the property change listener. You can write this to instantly print out any change made to the property:

meet.subject.addChangeListener((p) -> Log.p("New property value is: " + meet.subject.get()));

24.15.2. The cool stuff

That’s the simple stuff that can be done with properties, but they can do much more!

For starters all the common methods of Object can be implemented with almost no code:

public class Meeting implements PropertyBusinessObject {
     public final Property<Date,Meeting> when = new Property<>("when");
     public final Property<String,Meeting> subject = new Property<>("subject");
     public final Property<Integer,Meeting>  attendance = new Property<>("attendance");
     private final PropertyIndex idx = new PropertyIndex(this, "Meeting", when, subject, attendance);

    @Override
    public PropertyIndex getPropertyIndex() {
        return idx;
    }

    public String toString() {
        return idx.toString();
    }

    @Override
    public boolean equals(Object obj) {
        return obj != null && obj.getClass() == getClass() &&
            idx.equals(((Meeting)obj).getPropertyIndex());
    }

    @Override
    public int hashCode() {
        return idx.hashCode();
    }
}

This is easy thanks to introspection…​

You already have some simple code that can convert an object to/from JSON Maps for example: this can fill the property values from parsed JSON:

meet.getPropertyIndex().populateFromMap(jsonParsedData);

And vice versa:

String jsonString = meet.getPropertyIndex().toJSON();

You also have a simple ORM solution that maps values to table columns and can create tables. It’s no hibernate but sqlite isn’t big iron so it might be good enough.

Constructors

One of the problematic issues with constructors is that any change starts propagating everywhere. If you have fields in the constructor and you add a new field later you need to keep the old constructor for compatibility.

Codename One added a new syntax:

Meeting meet = new Meeting().
        subject.set("My Subject").
        when.set(new Date());

That’s why every property in the definition needed the Meeting generic and the set method returns the Meeting instance…​

You’re pretty conflicted on this feature and are thinking about removing it.

Without this feature the code would look like this:

Meeting meet = new Meeting();
meet.subject.set("My Subject");
meet.when.set(new Date());
Seamless serialization

Suppose you have an object called Contacts which includes contact information for example:

public class Contact implements PropertyBusinessObject {
    public final IntProperty<Contact> id  = new IntProperty<>("id");
    public final Property<String, Contact> name = new Property<>("name");
    public final Property<String, Contact> email = new Property<>("email");
    public final Property<String, Contact> phone = new Property<>("phone");
    public final Property<Date, Contact> dateOfBirth = new Property<>("dateOfBirth", Date.class);
    public final Property<String, Contact> gender  = new Property<>("gender");
    public final IntProperty<Contact> rank  = new IntProperty<>("rank");
    public final PropertyIndex idx = new PropertyIndex(this, "Contact", id, name, email, phone, dateOfBirth, gender, rank);

    @Override
    public PropertyIndex getPropertyIndex() {
        return idx;
    }

    public Contact() {
        name.setLabel("Name");
        email.setLabel("E-Mail");
        phone.setLabel("Phone");
        dateOfBirth.setLabel("Date Of Birth");
        gender.setLabel("Gender");
        rank.setLabel("Rank");
    }
}

Standard Java Objects can be serialized in Codename One by implementing the Codename One Externalizable interface. You also need to register the Externalizable object so the implementation will be aware of it. Codename One business objects are seamlessly Externalizable and you need to register them.

For example: you can do something like this in your init(Object) method:

new Contact().getPropertyIndex().registerExternalizable();

After you do that once you can write/read contacts from storage if you so want:

Storage.getInstance().writeObject("MyContact", contact);

Contact readContact = (Contact)Storage.getInstance().readObject("MyContact");

This will obviously also work for things like List<Contact> etc.

Seamless SQL storage

Writing SQL code can be tedious. Which is why SQLMap is such an important API for some of you. SQLMap allows CRUD (Create, Read, Update, Delete) operations on the built-in SQLite database using property objects.

If you continue the example from above to show persistence to the SQL database you can do something like this:

private Database db;
private SQLMap sm;
public void init(Object context) {
    theme = UIManager.initFirstTheme("/theme");
    Toolbar.setGlobalToolbar(true);
    Log.bindCrashProtection(true);

    try {
        Contact c = new Contact();
        db = Display.getInstance().openOrCreate("propertiesdemo.db"); (1)
        sm = SQLMap.create(db); (2)
        sm.setPrimaryKeyAutoIncrement(c, c.id); (3)
        sm.createTable(c); (4)
    } catch(IOException err) {
        Log.e(err);
    }
}

In the above code you do the following:

1Open or create the SQLite database. The name is an identifier rather than a path.
2Wrap it in a SQLMap, which is what maps properties to columns.
3Declare id the primary key and let the database assign it.
4Create the table if it isn’t there yet.
Notice that now altering a created table isn’t possible so if you add a new property you might need to detect that and do an alter call manually

You can then add entries to the contact table using:

sm.insert(myContact);

You can update an entry using:

sm.update(myContact);

And delete an entry using:

sm.delete(myContact);

Listing the entries is more interesting:

List<PropertyBusinessObject> contacts = sm.select(c, c.name, true, 1000, 0);

for(PropertyBusinessObject cc : contacts) {
    Contact currentContact = (Contact)cc;

   // ...
}

The arguments for the select method are:

  • The object type

  • The attribute by which you want to sort the result (can be null)

  • Whether sort is ascending

  • Number of elements to fetch

  • Page to start with - in this case if you have more than 1000 elements you can fetch the next page using sm.select(c, c.name, true, 1000, 1)

There are many more configurations where you can fine tune how a specific property maps to a column etc.

What’s still missing

The SQLMap API is simplistic and doesn’t try to be Hibernate/JPA for mobile. Basic things aren’t available now and won’t work. This isn’t necessarily a problem as mobile databases don’t need to be as powerful as server databases.

Relational Mappings/JOIN

There’s no way to map an object to another object in the database with the one-many or one-one relationships JPA offers. The SQLMap API isn’t suited to that level of mapping.

Threading

SQLite is sensitive to threading issues on iOS, so SQLMap issues its calls in process rather than moving them off the caller’s thread. That can be a problem for larger data sets, because the calls then run on the EDT.

Alter

Altering a table to match a schema that changed after the table was created isn’t supported. Migrating such a schema means issuing that SQL directly.

Complex SQL/Transactions

Functions, joins, transactions and a good deal of other SQL sit outside the mapping.

SQL can be used directly for any of that. A transaction begun before an insert, update or delete works as advertised, but the mapping is unaware of a rollback, so the data has to be re-fetched afterward.

The mapping covers autoincrement and the cases that map cleanly onto a table; a use case that needs more than that subset is better served by dropping to SQL directly.

Caching/Collision

Nothing is cached, so selecting the same row twice produces two separate instances. Update both and one of them wins, with no indication that the other was discarded.

That means the application has to keep track of the objects it holds, so a modified version isn’t left sitting beside an older one.

Preferences binding

Some objects make sense as global objects, you can use the Preferences API to store that data directly but then you don’t have the type safety that property objects bring to the table. That’s where the binding of property objects to preferences makes sense. For example: say you have a global Settings property object you can bind it to preferences using:

PreferencesObject.create(settingsInstance).bind();

If settings has a property called companyName it would bind into Preferences under the Settings.companyName entry.

You can do some more elaborate bindings such as:

PreferencesObject.create(settingsInstance).
    setPrefix("MySettings-").
    setName(settingsInstance.companyName, "company").
    bind();

This would customize all entry keys to start with MySettings- instead of Settings.. This would also set the company name entry to company so in this case instead of Settings.companyName you’d have MySettings-company.

UI binding

One of the bigger features of properties are their ability to bind UI to a property. For example: if you continue the sample above with the Contact class, say you have a text field on the form and you want the property (which you mapped to the database) to have the value of the text field. You could do something like this:

myNameTextField.setText(myContact.name.get());
myNameTextField.addActionListener(e -> myContact.name.set(myNameTextField.getText()));

That would work but what if you changed the property value, that wouldn’t be reflected in the text field?

Also that works for text field but what about other types for example: numbers, check boxes, pickers etc. This becomes a bit more tedious with those.

Binding makes this all seamless. For example: the code above can be written as:

UiBinding uib = new UiBinding();
uib.bind(myContact.name, myNameTextField);

The cool thing is that this works with many component types and property types almost magically. Binding works by using an adapter class to convert the data to/from the component. The adapter itself works with a generic converter for example: this code:

uib.bind(myContact.rank, myRankTextField);

Seems like the one above but it takes a String that’s returned by the text field and seamlessly converts it to the integer needed by rank. This also works in the other direction…​

You can build a UI that would allow you to edit the Contact property in memory:

Container resp = new Container(BoxLayout.y());
UiBinding uib = new UiBinding();

TextField nameTf = new TextField();
uib.bind(c.name, nameTf);
resp.add(c.name.getLabel()). (1)
        add(nameTf);

TextField emailTf = new TextField();
emailTf.setConstraint(TextField.EMAILADDR);
uib.bind(c.email, emailTf);
resp.add(c.email.getLabel()).
        add(emailTf);

TextField phoneTf = new TextField();
phoneTf.setConstraint(TextField.PHONENUMBER);
uib.bind(c.phone, phoneTf);
resp.add(c.phone.getLabel()).
        add(phoneTf);

Picker dateOfBirth = new Picker();
dateOfBirth.setType(Display.PICKER_TYPE_DATE); (2)
uib.bind(c.dateOfBirth, dateOfBirth);
resp.add(c.dateOfBirth.getLabel()).
        add(dateOfBirth);

ButtonGroup genderGroup = new ButtonGroup();
RadioButton male = RadioButton.createToggle("Male", genderGroup);
RadioButton female = RadioButton.createToggle("Female", genderGroup);
RadioButton undefined = RadioButton.createToggle("Undefined", genderGroup);
uib.bindGroup(c.gender, new String[] {"M", "F", "U"}, male, female, undefined); (3)
resp.add(c.gender.getLabel()).
        add(GridLayout.encloseIn(3, male, female, undefined));

TextField rankTf = new TextField();
rankTf.setConstraint(TextField.NUMERIC);
uib.bind(c.rank, rankTf); (4)
resp.add(c.rank.getLabel()).
        add(rankTf);
1Notice the use of the property’s label, which allows better encapsulation
2You can bind picker seamlessly
3You can bind many radio buttons to a single property to allow the user to select the gender, notice that labels and values can be different for example: "Male" selection will translate to "M" as the value
4Numeric bindings "work"
Properties form for the contact
Figure 242. Properties form for the contact
Binding object & auto commit

You skipped a couple of fact about the bind() method. It has an additional version that accepts a ComponentAdapter which allows you to adapt the binding to any custom 3rd party component. That’s a bit advanced for now but I might discuss this later.

For example, the big thing "skipped" earlier was the return value…​ bind returns a UiBinding.Binding object when performing the bind. This object allows you to manipulate aspects of the binding specifically unbind a component and also manipulate auto commit for a specific binding.

Auto commit determines if a property is changed instantly or on commit. This is useful for a case where you have an "OK" button and want the changes to the UI to update the properties when "OK" is pressed (this might not matter if you keep different instances of the object). When autocommit is on (the default which you can change through setAutoCommit in the UiBinding) changes reflect instantly, when it’s off you need to explicitly call commit() or rollback() on the Binding class.

commit() applies the changes in the UI to the properties, rollback() restores the UI to the values from the properties object (useful for a "reset changes" button).

Binding also includes the ability to "unbind" this is important if you have a global object that’s bound to a UI that’s discarded. Binding might hold a hard reference to the UI and the property object might create a memory leak.

By using the disconnect() method in Binding you can separate the UI from the object and allow the GC to clean up.

UI generation

Up until now this was pretty cool but if you looked at the UI construction code above you would see that it’s pretty full of boilerplate code. The thing about boilerplate is that it shows where automation can be applied, that’s the exact idea behind the magical "InstantUI" class. This means that the UI above can be generated using this code:

InstantUI iui = new InstantUI();
iui.excludeProperty(myContact.id); (1)
iui.setMultiChoiceLabels(myContact.gender, "Male", "Female", "Undefined"); (2)
iui.setMultiChoiceValues(myContact.gender, "M", "F", "U");
Container cnt = iui.createEditUI(myContact, true); (3)
1The id property is useful for database storage but you want to exclude it from the UI
2This implements the gender toggle button selection, you provide a hint to the UI so labels and values differ
3You create the UI from the screenshot above with one line and it’s seamlessly bound to the properties of myContact. The second argument indicates the "auto commit" status.

This still carries most of the flexibilities of the regular binding for example: you can still get a binding object using:

UiBinding.Binding b = iui.getBindings(cnt);

You might not have noticed this but in the previous verbose code you had lines like:

emailTf.setConstraint(TextField.EMAILADDR);

You might be surprised to know that this will still work seamlessly without doing anything, as would the picker component used to pick a date…​

The picker component implicitly works for date type properties, numeric constraints and numbers are implicitly used for number properties and check boxes are used for booleans.

However, how do you know to use an email constraint for the email property?

You’ve some special case defaults for some common property names, so if your property is named email it will use an email constraint by default. If it’s named url or password etc. It will do the "right thing" unless you explicitly state otherwise. You can customize the constraint for a specific property using something like:

iui.setTextFieldConstraint(myContact.email, TextArea.ANY);

This will override the defaults you’ve in place. The goal of this tool is to have sensible "magical" defaults that "work."

25. Deeper Network Connectivity

Codename One ships a set of APIs under com.codename1.io.wifi, com.codename1.io.bonjour, com.codename1.io.usb and an extension of com.codename1.io.NetworkManager that lets apps inspect and manage the device’s local network beyond plain HTTP.

A quick connectivity check is NetworkManager.getInstance().isConnected() — it reads the cached platform network state and returns immediately without an HTTP probe, so it’s safe to call from the EDT before deciding whether to fire a ConnectionRequest.

These APIs share three guarantees:

  • Every callback runs on the EDT so you can update UI directly from the result.

  • The build pipeline injects the matching Android permissions, iOS entitlements, and NSXxxUsageDescription / NSBonjourServices Info.plist entries automatically by scanning the classpath. Apps that never reference these classes see no manifest or entitlement changes.

  • The simulator implements best-effort versions of each API so you can wire and test your UI without a device. The simulator also prints the list of permissions/entitlements each API will need in a real build the first time you call it.

The simulator’s "API usage report" prints to stdout on JVM shutdown so you can spot anything you pulled in via a transitive cn1lib.

25.1. WiFi information

The fastest way to know whether the user is on WiFi, what SSID they joined, and what their local IP is:

if (WiFi.isInfoSupported()) {
    String ssid = WiFi.getCurrentSSID();   // e.g. "Codename One"
    String bssid = WiFi.getBSSID();        // "aa:bb:cc:11:22:33"
    String gw = WiFi.getGateway();         // "192.168.1.1"
    String ip = WiFi.getIp();              // "192.168.1.42"
}

Any of these calls may return null if the device isn’t on WiFi, if the user denied a runtime permission, or if the platform never exposed the value to apps. The framework normalizes the wrapping that Android adds to the SSID and treats <unknown ssid> and the obfuscated BSSID 02:00:00:00:00:00 as null.

25.1.1. Required permissions and entitlements

The build pipeline injects the following automatically the first time it sees com.codename1.io.wifi.WiFi on the classpath:

PlatformInjectedNotes

Android

ACCESS_WIFI_STATE, ACCESS_NETWORK_STATE

"normal" permissions; no runtime prompt

Android API 26+

ACCESS_FINE_LOCATION (maxSdkVersion=32)

Required by the OS to return a real SSID

Android API 33+

NEARBY_WIFI_DEVICES with neverForLocation

Replaces fine-location for scan flows

iOS

com.apple.developer.networking.wifi-info entitlement

Required since iOS 13

iOS

NSLocationWhenInUseUsageDescription

iOS still checks CoreLocation behind the scenes

You can override any injected automatically value via the standard build hints:

ios.locationUsageDescription=Allow access to read your Wi-Fi network name to discover printers on your network.
android.xpermissions=<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

25.2. WiFi scan

Scanning returns an array of `WiFiNetwork`s sorted by signal strength. The callback fires once.

iOS doesn’t expose a public WiFi scan API. On iOS WiFi.scan(…​) always reports an UnsupportedOperationException. Plan for a graceful fallback if your UI exposes a scan button.

Android throttles scans to 4 per 2 minutes per foreground app on API 28+; throttled scans return cached results, not an error.

25.3. WiFi connect

To associate the device with a specific SSID:

WiFi.connect("MyAccessPoint", "s3cret!", WiFiSecurity.WPA_PSK,
        new WiFiConnectCallback() {
    @Override
    public void onConnectResult(boolean connected, Throwable error) {
        if (!connected) {
            Log.e(error);
            return;
        }
        // ssid is now joined
    }
});
  • On Android 10+ the OS shows a system confirmation dialog with the SSID before joining; this dialog can’t be bypassed.

  • On Android 9 and below the framework falls back to the legacy WifiConfiguration API and joins without an in-app prompt.

  • On iOS 11+ this uses NEHotspotConfiguration. The user is shown a system prompt the first time the app tries to join the SSID; subsequent attempts reuse the cached configuration.

  • WiFiSecurity must match the AP’s actual security mode (OPEN, WEP, WPA_PSK, WPA3_SAE). Passing the wrong mode causes the join to fail.

WiFi.disconnect(ssid) releases the request on Android 10+ and removes the hotspot configuration on iOS.

On iOS the build pipeline also enables the NetworkExtension.framework and the HotspotConfiguration entitlement only when it sees a call to WiFi.connect(…​) or WiFi.disconnect(…​). Apps that only read SSID/BSSID won’t pull in the framework, which keeps App Store API-usage scans clean.

25.4. Bonjour / mDNS discovery

BonjourBrowser watches the local network for services of a given mDNS type. BonjourPublisher advertises a service.

BonjourBrowser browser = BonjourBrowser.browse("_http._tcp.",
        new BonjourServiceListener() {
    @Override
    public void onServiceResolved(BonjourService s) {
        Log.p("Found " + s.getName() + " at " + s.getHost() + ":" + s.getPort());
    }
    @Override
    public void onServiceLost(BonjourService s) { /* null */ }
    @Override
    public void onBrowseError(Throwable t) { Log.e(t); }
});

// when done:
browser.stop();

To advertise an HTTP server you wrote on port 8080:

BonjourPublisher pub = BonjourPublisher.publish(
        "My Server", "_http._tcp.", 8080, null);

// later:
pub.unpublish();

25.4.1. iOS requirements

Bonjour on iOS 14+ returns no results unless:

  • The Info.plist NSLocalNetworkUsageDescription key explains why the app needs local-network access.

  • The Info.plist NSBonjourServices array lists each service type the app expects to find.

The build pipeline injects both keys automatically when com.codename1.io.bonjour is on the classpath, with _http._tcp. as the seed type. To publish or browse a custom type, set the build hint:

ios.NSBonjourServices=_myapp._tcp.,_http._tcp.
ios.NSLocalNetworkUsageDescription=Discover other instances of MyApp on this Wi-Fi network.

The comma-separated values are expanded into the Info.plist array at build time.

25.4.2. Android requirements

The pipeline adds CHANGE_WIFI_MULTICAST_STATE automatically. NsdManager does the rest; there is no runtime permission prompt.

25.4.3. Simulator behaviour

If javax.jmdns.JmDNS is on the simulator classpath, browse calls dispatch through it. Otherwise the listener receives a single UnsupportedOperationException and the simulator prints a hint. Add JmDNS to your application’s executable-jar / simulator profile (typically the <profile id="executable-jar"> block in your cn1app’s common/pom.xml or your javase/pom.xml) to exercise real discovery in the simulator:

<!-- in YOUR app's pom, NOT in the framework / common pom that gets shipped
     to the device. The cn1app archetype's executable-jar profile is the
     right place because it only applies when launching the simulator. -->
<dependency>
    <groupId>org.jmdns</groupId>
    <artifactId>jmdns</artifactId>
    <version>3.5.9</version>
    <scope>runtime</scope>
</dependency>

Don’t add JmDNS to the cn1app common/pom.xml outside a simulator-scoped profile — it would then ship with your Android / iOS binaries even though both platforms supply native mDNS implementations.

25.5. Network type change events

Subscribe to network type transitions (WiFi <→ Cellular <→ Ethernet <→ None):

NetworkManager.getInstance().addNetworkTypeListener(new NetworkTypeListener() {
    @Override
    public void onNetworkTypeChanged(int oldType, int newType, boolean vpnActive) {
        if (newType == NetworkManager.NETWORK_TYPE_NONE) {
            // offline -- queue uploads for later
        }
        if (vpnActive) {
            // refuse to roam to corporate-only resources
        }
    }
});

The current snapshot is available without subscribing:

int type = NetworkManager.getInstance().getCurrentNetworkType();
boolean vpn = NetworkManager.getInstance().isVPNActive();

The platform implementations are:

  • Android: ConnectivityManager.registerNetworkCallback on API 21+, falling back to the legacy CONNECTIVITY_ACTION broadcast on older devices.

  • iOS: SCNetworkReachability with a kernel callback scheduled on the main run loop.

  • Simulator: derives the type from NetworkInterface.getDisplayName; transitions aren’t synthesized.

VPN detection is best-effort and intentionally heuristic. On Android it uses NetworkCapabilities.TRANSPORT_VPN plus an interface-name probe; on iOS it inspects utun*, tun*, ppp* and ipsec* interfaces. Use it as a hint, not as a security boundary.

25.6. WiFi Direct (Wi-Fi P2P)

WiFi Direct lets two devices form a peer-to-peer link without an access point. The API is Android-only — iOS uses MultipeerConnectivity for similar scenarios and is intentionally out of scope.

The build pipeline injects CHANGE_WIFI_STATE, ACCESS_WIFI_STATE, ACCESS_NETWORK_STATE, ACCESS_FINE_LOCATION and NEARBY_WIFI_DEVICES automatically when WiFiDirect is on the classpath, plus a <uses-feature android:name="android.hardware.wifi.direct"/> declaration so devices without the radio can still install the app via Play Store filtering.

25.7. USB host

The USB API in com.codename1.io.usb lets the device act as a USB host and talk to attached peripherals — a barcode scanner, a serial converter, a microcontroller. It’s Android-only; iOS has no third-party USB host access and the simulator stubs everything out.

if (!Usb.isSupported()) { return; }

Usb.addDeviceListener(new UsbDeviceListener() {
    @Override
    public void onDeviceAttached(UsbDevice d) {
        if (d.getVendorId() == 0x0403 && d.getProductId() == 0x6001) {
            Usb.requestPermission(d);
        }
    }
    @Override
    public void onDeviceDetached(UsbDevice d) { }
    @Override
    public void onPermissionResult(UsbDevice d, boolean granted) {
        if (granted) {
            try (InputStream in = Usb.openInputStream(d, 0x81);
                 OutputStream out = Usb.openOutputStream(d, 0x02)) {
                out.write("AT\r\n".getBytes());
                byte[] buf = new byte[64];
                int n = in.read(buf);
                // null
            } catch (IOException e) { Log.e(e); }
        }
    }
});

The build pipeline injects:

  • <uses-feature android:name="android.hardware.usb.host" android:required="false"/>.

If you want the OS to launch your app when a matching device is plugged in, ship a device_filter.xml resource and declare an <intent-filter> via the android.xintent_filter build hint:

<!-- native/android/res/xml/device_filter.xml -->
<resources>
    <usb-device vendor-id="1027" product-id="24577" />
</resources>
android.xintent_filter=<intent-filter><action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" /></intent-filter><meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" android:resource="@xml/device_filter" />

25.8. Releasing platform resources

Most of these APIs hold background resources (broadcast receivers, network callbacks, NSNetService delegates) that you must release explicitly:

  • BonjourBrowser — call .stop().

  • BonjourPublisher — call .unpublish().

  • WiFiDirect — call .stopDiscovery() and .disconnect().

  • WiFi.connect(…​) — call WiFi.disconnect(ssid) when the user navigates away.

  • Usb.addDeviceListener(…​) — call removeDeviceListener(…​).

  • NetworkManager.addNetworkTypeListener(…​) — call removeNetworkTypeListener(…​).

Each of these keeps a radio or system service awake. Bonjour browsers and WiFi scans in particular drain battery noticeably — they’re fine during a few user-facing screens but shouldn’t stay armed for the lifetime of the app. Release them as soon as the screen that needs them is no longer visible.

26. JSON / XML Object Mapping

Codename One includes a build-time POJO mapping framework that turns a plain Java class — or a PropertyBusinessObject — into JSON or XML without any runtime reflection. The Maven plugin scans the project’s compiled bytecode at build time, generates a typed Mapper next to each @Mapped class, and ships it alongside the source. The runtime entry point is two methods on com.codename1.mapping.Mappers.

The framework sits next to the existing JSONParser, XMLParser and PropertyIndex.toJSON / fromJSON helpers — it doesn’t replace them. Use the imperative APIs when you want to read a foreign JSON shape without modelling it; use the annotation framework when you have a model class and want a one-liner round-trip.

26.1. Annotate the model

Apply @Mapped to the class. The processor accepts POJOs with public fields, PropertyBusinessObject-style classes with Property fields, or any mix:

@Mapped
@XmlRoot("user")
public class User {

    @JsonProperty("first_name")
    @XmlElement("first")
    public String firstName;

    public int age;

    // Renders as <user role="admin"/> in XML; omitted from JSON.
    @XmlAttribute
    @JsonIgnore
    public String role;

    public User() { }                                  (1)
}
1Every @Mapped class must declare a public no-arg constructor — the generated mapper calls it from fromJson / fromXml.

For PropertyBusinessObject-style classes the same annotations work on Property<T> fields. The processor reads through Property#get / Property#set so subscribers (addChangeListener) still see the mutation:

@Mapped
public class Item implements PropertyBusinessObject {
    public final Property<String, Item>  name = new Property<>("name");
    public final Property<Integer, Item> qty  = new Property<>("qty");

    private final PropertyIndex idx =
            new PropertyIndex(this, "Item", name, qty);

    public PropertyIndex getPropertyIndex() { return idx; }

    public Item() { }
}

26.1.1. Field-level annotations

AnnotationPurpose

@JsonProperty("k")

Rename the JSON key. Default: the field name.

@JsonIgnore

Omit the field from JSON.

@XmlElement("name")

Rename the XML element. Default: the field name.

@XmlAttribute("name")

Lift the field into an XML attribute rather than a nested child element. Only valid for scalar or Property<scalar> fields.

@XmlTransient

Omit the field from XML.

26.1.2. Supported field types

KindDetail

Scalars

String, all primitives, boxed primitives, java.util.Date, byte[] (base64 in JSON / XML).

Property<T, ?>

T must be a scalar or another @Mapped type.

ListProperty<T, ?>

Same rules as Property.

java.util.List<T>

Read from the field’s generic signature.

Nested @Mapped ref.

Recursive serialization through the registered nested mapper.

26.2. Round-trip

Mappers.fromJson and Mappers.fromXml both accept either a String or a java.io.Reader, so streamed responses (network, file, resource) don’t need to be fully buffered first.

26.3. How the plumbing works

At build time:

  1. cn1:process-annotations (PROCESS_CLASSES phase) scans target/classes for every class carrying @Mapped.

  2. The processor validates each one (concrete, public no-arg constructor, supported field types) and fails the build on the first offender — every offending class shows up in the same error report.

  3. A <SimpleName>Cn1Mapper class is generated for every @Mapped type in the same package as the source class (e.g. com.example.UserCn1Mapper next to com.example.User). Each generated mapper exposes a public static register() hook.

  4. A single cn1app.MapperBootstrap is generated whose constructor calls UserCn1Mapper.register(), ItemCn1Mapper.register(), …​ for every accepted @Mapped class.

At app start:

  • On iOS / Android, the build server probes the project zip for cn1app/MapperBootstrap.class. When found it splices new cn1app.MapperBootstrap(); into the per-build application stub right before Display.init. ParparVM rename and R8 obfuscation rewrite the call site and the generated class together, so the direct symbol reference stays valid after the pass.

  • On the JavaSE simulator and desktop run, JavaSEPort#postInit loads the bootstrap via Class.forName("cn1app.MapperBootstrap"). The classloader is the legitimate path here — JavaSE runs unobfuscated and the same Class.forName pattern is used by the @Route dispatcher.

Projects that ship no @Mapped classes produce no bootstrap, the build server probe falls through, and JavaSEPort catches the ClassNotFoundException — the registry stays empty and Mappers.toJson throws a clear "no mapper registered" error.

The runtime registry is keyed on Class#getName(). The map keys survive obfuscation because both the registration site and the lookup site see the same renamed name within a single execution — the keys are never persisted across builds.

26.4. Custom mappers

Sometimes a class lives in a third-party JAR the build can’t annotate. Hand-write a Mapper<T> and register it at startup:

// A type from a third-party jar the build cannot annotate.
static class LatLon {
    double lat;
    double lon;
}

static class LatLonMapper implements Mapper<LatLon> {
    @Override
    public Class<LatLon> type() {
        return LatLon.class;
    }

    @Override
    public Map<String, Object> toMap(LatLon instance) {
        Map<String, Object> m = new LinkedHashMap<String, Object>();
        m.put("lat", Double.valueOf(instance.lat));
        m.put("lon", Double.valueOf(instance.lon));
        return m;
    }

    @Override
    public LatLon fromMap(Map<String, Object> map) {
        LatLon out = new LatLon();
        out.lat = readDouble(map.get("lat"));
        out.lon = readDouble(map.get("lon"));
        return out;
    }

    @Override
    public String xmlRootName() {
        return "latLon";
    }

    @Override
    public void writeXml(LatLon instance, Element root) {
        root.setAttribute("lat", String.valueOf(instance.lat));
        root.setAttribute("lon", String.valueOf(instance.lon));
    }

    @Override
    public LatLon readXml(Element root) {
        LatLon out = new LatLon();
        out.lat = Double.parseDouble(root.getAttribute("lat"));
        out.lon = Double.parseDouble(root.getAttribute("lon"));
        return out;
    }

    // JSONParser hands back a Double for every number, but a map that came
    // from somewhere else may hold any Number. Read through the interface
    // rather than casting to Double: a failed cast does not throw on iOS,
    // so the catch you would write for it never runs.
    private double readDouble(Object value) {
        return value instanceof Number ? ((Number) value).doubleValue() : 0;
    }
}

void registerMappers() {
    Mappers.register(new LatLonMapper());
}

Hand-written mappers take precedence over generated ones for the same class.

27. Component Binding

The component binding framework wires a model field to the matching component in a Form or Container by name. The Maven plugin generates a <SimpleName>Cn1Binder per @Bindable class at build time, so the wiring happens through direct symbol references — no reflection, no listener bookkeeping in application code.

It’s a thin alternative to the imperative UiBinding API (com.codename1.properties.UiBinding). Both can be used together.

27.1. Annotate the model

@Bindable
public class LoginModel {

    @Bind(name = "userField", attr = BindAttr.TEXT)
    @Required
    private String user;
    public String getUser()              { return user; }
    public void   setUser(String u)      { this.user = u; }                  (1)

    @Bind(name = "rememberMe", attr = BindAttr.SELECTED)
    public boolean remember;                                                   (2)

    @Bind(name = "banner", attr = BindAttr.UIID, twoWay = false)
    public String bannerStyle;

    @Bind(name = "fullName",
          attr = BindAttr.TEXT,
          getter = "computeFullName",
          setter = "applyFullName")                                           (3)
    private String fullName;
    // both run during the initial bind, against a model that may still be
    // empty, so neither can assume a value is present
    public String computeFullName()      { return fullName == null ? "" : fullName.toUpperCase(); }
    public void   applyFullName(String f){ this.fullName = f == null ? null : f.trim(); }
}
1JavaBeans getUser / setUser are detected automatically when the field is private. The processor instruments setUser to fire a change notification for two-way bindings.
2Public field still works — direct field access falls back when no accessor matches the JavaBeans convention.
3Explicit getter / setter override the JavaBeans search. Use when the convention doesn’t apply (renamed accessors, transforms, fluent setters).

Every @Bind(name=…​) is looked up against Component#getName(), walking the container’s children recursively until a match is found. The default lookup matches GUI-builder names exactly.

27.1.1. Attribute kinds

BindAttrMirrors

TEXT

getText / setText on Label, Button, TextField, TextArea, SpanLabel, SpanButton. Two-way for TextArea / TextField.

UIID

getUIID / setUIID. One-way.

VISIBLE

isVisible / setVisible. One-way.

HIDDEN

isHidden / setHidden. One-way.

ENABLED

isEnabled / setEnabled. One-way.

SELECTED

isSelected / setSelected on CheckBox, RadioButton. Two-way.

ICON_NAME

Calls Resources.getGlobalResources().getImage(name) and writes the result via Label#setIcon. One-way.

NAME

setName. One-way.

27.2. Bind at runtime

Binding is the handle the binder returns:

MethodPurpose

refresh()

Push the current model values into every bound component. Use after mutating the model outside the form, or to re-sync after commit().

commit()

Pull current component values back into the model. Useful before validating / submitting when none of the bindings is two-way.

disconnect()

Remove every listener the binder installed and unregister the binding from the change-notification fan-out.

27.3. Two-way bindings and the change-notification contract

A binding declared twoWay = true flows in both directions:

How the update region breaks the two-way loop
  1. Component → model. The generated binder installs a listener on the editable component (TextField, CheckBox, etc.). When the user mutates the component, the binder calls the model’s setter (or assigns the public field). The setter’s instrumented exit calls Binders.notifyChanged(this).

  2. Model → component. Application code that mutates the model through a setter causes Binders.notifyChanged(this) to fire, which walks every live binding for that model and pushes the new value into the matching component.

The two paths together would loop forever if left alone — the model setter fires a change event, which refreshes the component, which fires its own change event, which calls the model setter again, …​ To break the loop, every framework-initiated mutation runs inside an update region guarded by a thread-local depth counter. While the counter is positive, notifyChanged is a no-op and component listeners short-circuit.

Concretely:

  • Binders.bind(model, container) enters an update region for the initial model → component push.

  • Binding#refresh() enters an update region for every subsequent model → component push.

  • Binding#commit() enters an update region for the component → model pull.

  • The generated component listener enters an update region before calling the setter.

27.3.1. Setter instrumentation

For every two-way @Bind field whose write accessor is a method (whether you wrote @Bind(setter="setName") or the processor detected setName(String) via the JavaBeans convention), the cn1:process-annotations Mojo reads the original .class file with ASM and inserts the equivalent of:

public void setName(String name) {
    this.name = name;
    com.codename1.binding.Binders.notifyChanged(this);   // injected
}

The injection lands before every return point of the setter. The instrumented setter is written back to target/classes, replacing the original. Application code that calls model.setName(…​) — from any thread, any code path — now triggers the binding fan-out automatically.

27.3.2. Limitation — cross-field setter chains

When a setter synchronously mutates a second bound field (setFirstName also calls setFullName, for example), whether the second field’s notifyChanged is suppressed depends on who called the first. An application calling setFirstName directly isn’t inside an update region, so the nested setFullName notifies at depth zero and its component refreshes normally. Reached from the generated component listener or from commit(), the chain runs inside a region that’s already open, the second field’s notifyChanged lands in it and is suppressed, and the bound component for fullName won’t catch up until something exits the region and a separate event drives the refresh. If you need the cross-field update to propagate, call binding.refresh() explicitly after the setter chain completes, or restructure the model so the cross-field mutation happens outside the setter’s call frame — for example via a Display.callSerially.

commit() is worse than suppression, and only one of the two remedies survives it. When both fields are bound two-way and fullName is emitted after firstName, the generated _commit carries on to the fullName component, reads the value still sitting in it, and calls setFullName — overwriting what setFirstName just derived. An immediate binding.refresh() afterward publishes the overwritten value rather than the derived one, so that remedy doesn’t help here.

The deferred write still does. Display.callSerially() queues the write and returns, so _commit finishes and leaves its update region before the deferred setFullName runs and notifies at depth zero.

Binding the derived field one way stops that write but doesn’t finish the job: one-way fields aren’t instrumented, and the setFirstName notification is still suppressed by the commit’s region, so the model holds the derived value while the screen keeps the old one. If you go that way, call binding.refresh() after the commit to publish it.

27.4. POJOs versus Property objects

A String, int, or boolean field is read and written through the resolved accessors. A Property<T, ?> field is read through get() and written through set() — so existing PropertyChangeListener subscribers fire as expected. Both styles can sit on the same class.

27.5. Validation annotations

The same @Bind field can carry validation annotations that are wired into a com.codename1.ui.validation.Validator. The validator is built by the generated binder at the moment you call Binders.bind(…​) and is reachable via Binding#getValidator() — so the same generated plumbing that pushes model values into components also installs the constraints that gate them.

AnnotationMaps toNotes

@Required

LengthConstraint(1)

"Field must be non-empty."

@Length(min = N)

LengthConstraint(N, message)

Minimum string length.

@Regex(pattern = …​, message = …​)

RegexConstraint(pattern, message)

Same dialect as com.codename1.util.regex.RE.

@Email

RegexConstraint.validEmail(message)

The standard email regex (also accepts empty — stack with @Required).

@Url

RegexConstraint.validURL(message)

http / https / ftp / file schemes.

@Numeric(decimal = …​, min = …​, max = …​, message = …​)

NumericConstraint(…​)

Bounds are inclusive; default range is unbounded.

@ExistIn({…​})

ExistInConstraint(values, caseSensitive, message)

Whitelist of allowed string values.

@Validate(MyConstraint.class)

new MyConstraint()

Escape hatch — the class must have a public no-arg constructor and implement Constraint.

Multiple annotations on the same field are combined into a GroupConstraint (first failure wins), matching the behaviour of Validator.addConstraint(Component, Constraint…​).

After binding, drive validation through the returned handle:

// set this first: bind() attaches the constraints, and each one picks a
// data-change or an action listener from this flag as it attaches
Validator.setValidateOnEveryKey(true);

LoginModel model = new LoginModel();
Binding b = Binders.bind(model, form);

// Auto-disable a submit button until everything is valid. Pass the
// button you built the form with: Container has no lookup by name.
b.getValidator().addSubmitButtons(submitButton);

// Programmatic gate before saving:
if (b.getValidator().isValid()) {
    repository.save(model);
}

Binding#getValidator() never returns null — when the model has no validation annotations the validator is empty and isValid() returns true. The validator owns the listener and emblem plumbing on the matching components; calling addSubmitButtons after bind is the typical pattern.

27.6. Build-time validation

In addition to the runtime constraints above, the annotation processor itself fails the build when:

  • @Bind is applied to a field with no accessible read path — the field must be public, declare a JavaBeans getX / isX getter, or the annotation must name an explicit getter.

  • A two-way @Bind field on a TEXT or SELECTED attr has no writable accessor. Set twoWay=false to keep the binding one-way, or add a setter / explicit setter=.

  • @Bind(name=…​) is empty.

  • The field’s static type isn’t supported (raw collections, opaque references that aren’t @Bindable themselves, …​).

  • @Regex is missing a pattern, or @ExistIn is missing values.

Errors are accumulated so a single build run reports every offending field at once.

27.7. How the plumbing works

cn1:process-annotations writes one <SimpleName>Cn1Binder per @Bindable class in the source class’s package, plus a single cn1app.BinderBootstrap whose constructor calls UserCn1Binder.register(), LoginModelCn1Binder.register(), …​ for every accepted @Bindable class. At app start:

  • On iOS / Android the build server probes the project zip for cn1app/BinderBootstrap.class and splices new cn1app.BinderBootstrap(); into the per-build application stub before Display.init. ParparVM rename and R8 obfuscation rewrite the direct symbol reference together with the generated class so the binding still resolves after the pass.

  • On JavaSE JavaSEPort#postInit loads the bootstrap via Class.forName("cn1app.BinderBootstrap") — the unobfuscated classloader path.

Projects with no @Bindable classes produce no bootstrap; the build server probe falls through and the registry stays empty.

The runtime registry is keyed on Class#getName(); obfuscation renames the call sites and the registered keys together within a single execution.

28. SQLite ORM

The SQLite ORM framework turns a plain Java class into a typed data access object at build time. The Maven plugin reads @Entity / @Id / @Column annotations from the project’s compiled bytecode, generates one <SimpleName>Cn1Dao per entity in the source class’s package, and hands the dao to com.codename1.orm.EntityManager for typed retrieval. The dao issues prepared statements through com.codename1.db.Database, the same surface every cn1 SQLite port exposes.

The framework is a JPA-inspired, simplified alternative to the existing imperative SQLMap. It doesn’t replace SQLMap; both can be used side by side.

28.1. Annotate the entity

@Entity(table = "users")
public class User {

    @Id(autoIncrement = true)
    public long id;

    @Column(name = "full_name", nullable = false)
    public String name;

    public int age;

    public java.util.Date createdAt;

    @DbTransient
    public String cacheKey;                            (1)

    public User() { }
}
1Excluded from the generated table.

Property fields work the same way; the dao calls Property#get and Property#set for read and write.

28.1.1. Field-level annotations

AnnotationPurpose

@Id

Marks a primary-key field. The legacy DAO uses one key; managed sessions also support composite assigned keys. autoIncrement=true (the default) emits INTEGER PRIMARY KEY AUTOINCREMENT and back-fills the field after insert.

@Column(name)

Rename the column. Default: the field name.

@Column(type)

Override the SQL type. Default: inferred from the Java type (String → TEXT, int/long → INTEGER, float/double → REAL, boolean → INTEGER, java.util.Date → INTEGER (epoch millis), byte[] → BLOB).

@Column(nullable=false)

Adds NOT NULL to the column declaration.

@DbTransient

Excludes the field from the table.

28.2. Use the dao

EntityManager is a thin façade over Database. The underlying connection is reachable through em.database() for raw SQL when the dao surface isn’t enough. Transactions:

em.beginTransaction();
try {
    users.insert(u1);
    users.insert(u2);
} catch (IOException e) {
    em.rollbackTransaction();
    throw e;
}
// outside the catch on purpose: a commit that fails has already ended
// the transaction, so rolling back here would throw "No transaction is
// in progress" over the top of the real failure
em.commitTransaction();

28.3. Managed sessions and relationships

The managed ORM layer uses the same entity annotations on the client and the backend. New mappings, including relationships, @Version, and @GeneratedValue, live in com.codename1.annotations.db. The existing @Entity, @Id, @Column, and @DbTransient annotations remain in com.codename1.annotations. Open a com.codename1.orm.session.Session with EntityManager.openSession(). Keep one session per unit of work. A session is not thread-safe; a backend pooled entity manager can still be shared between requests, with a separate session for each request.

The existing scalar Dao API keeps its immediate-write behavior. Entities using relationships, @Version, composite IDs, generated strategies, inheritance, converters, indexes, or embedded values use sessions; asking for a legacy DAO for these mappings throws an error rather than dropping managed behavior.

A managed session maintains one instance per entity identity, detects changes at flush time, and flushes before commit and before queries in a transaction. persist, merge, and remove require an active transaction. merge returns the managed copy; it doesn’t attach the supplied detached instance. Merging an entity already scheduled for removal is rejected.

Session session = em.openSession();
try {
    session.beginTransaction();
    Customer customer = session.find(Customer.class, customerId);
    customer.name = "Updated name";
    session.commitTransaction();
} finally {
    session.close();
}

The public Session, Query, and JpqlQuery interfaces describe application operations. Generated metadata, field-access hooks, and SQL adapters live under com.codename1.impl.orm; application code shouldn’t depend on those classes.

Closing a session rolls back any active transaction. It never commits. A rollback clears the persistence context. An optimistic-lock or database failure requires rollback before the session can continue writing if the database still has an active transaction. When a failed client commit has already ended the underlying transaction, the session detaches its entities and becomes inactive. Check isTransactionActive() before rolling back; an inactive session can start a new transaction. Closing the session doesn’t close its entity manager’s database or pool. Backend Database.beginTransaction() reserves the connection to the initiating thread until commit, rollback, or close. Other threads wait; the initiating thread must finish the transaction.

28.3.1. Association mappings

Use @ManyToOne, @OneToOne, @OneToMany, and @ManyToMany on public entity fields. To-many fields use List<T>, Set<T>, or Map<K,T>. @JoinColumn names an owning foreign-key column. mappedBy identifies the owning field on the other entity. An owning to-many relationship uses a join table; @JoinTable customizes its name and key columns.

To-one relationships default to FetchType.EAGER; to-many relationships default to FetchType.LAZY. Override these defaults with the annotation’s fetch member. CascadeType selects the persistence operations to propagate. orphanRemoval=true removes children dropped from an initialized relationship. Application code maintains both sides of bidirectional object relationships. Removal cascades on inverse to-one and to-many relationships account for pending owning-side moves, including join-table changes, before choosing which children to remove.

The build enhances entity classes and field-access callers so a normal public field read can initialize a lazy relationship. No runtime proxy generator or reflection is needed. session.isLoaded(entity, "relationship") inspects load state without fetching; session.initialize(entity, "relationship") fetches explicitly. An unloaded relationship can’t be read after detaching its entity or closing its session: this throws LazyInitializationException. Generated @Mapped serialization code rejects unloaded associations rather than fetching them during serialization; initialize the relationships needed for the output first.

Fetching is synchronous. On the client, perform database work off the EDT and initialize the data needed by the UI before closing the session. Already-loaded data remains readable after the session closes.

28.3.2. Queries and counters

The session query builder uses Java field paths, including relationship paths:

List<Order> orders = session.query(Order.class)
    .eq("customer.name", "Alice")
    .orderBy("id", true)
    .limit(20)
    .fetch("customer")
    .list();

fetch overrides laziness for a direct relationship. To-one and inverse foreign-key collection fetches with single-column keys are batched. Ordered, map, composite-key, and join-table collections currently use individual loaders. Root pagination happens before loading fetched collections. query.count() counts matching entities without loading them and ignores pagination, ordering, and joins needed only by ordering. session.count(entity, "relationship") counts a relationship without materializing it.

createQuery accepts an explicit JPQL subset: entity or scalar selects, relationship joins, named parameters, comparisons and boolean predicates, aggregates, grouping/having, ordering, nested queries in scalar predicates, and bulk update/delete. String and numeric literals and named values become bound SQL parameters. IN :values and IN (:values) accept an Iterable or object array, including an empty collection. Unsupported syntax is rejected. Multiple scalar projections return Object[]. Entity names, field names, aliases, and named parameters accept Java identifier characters, including supplementary Unicode letters. Comparisons require at least one operand with a known storage type; two untyped parameters can’t be compared directly. IS NULL and IS NOT NULL also require a known operand type; a standalone untyped parameter is rejected. A scalar nested query requires an aggregate without GROUP BY, so it returns at most one row. IN and EXISTS may use queries that return multiple rows. A DISTINCT query can order only by selected expressions. Text range comparisons and LIKE patterns are case-sensitive across supported databases. An explicit LIKE …​ ESCAPE uses a literal or named parameter for both the pattern and the escape character.

List<Order> orders = session.createQuery(
    "select o from Order o where o.customer.name = :name order by o.id",
    Order.class).setParameter("name", "Alice").list();

Bulk mutations use executeUpdate() inside a transaction. Repeated assignment targets are rejected during query creation. Aggregate functions are allowed inside scalar nested queries, but not directly in assignments or bulk predicates. On MySQL and MariaDB, nested queries that read the bulk mutation target table are rejected during query creation. They flush pending managed changes, bypass per-entity callbacks and cascades, and clear the context so later reads can’t reuse stale entities. Bulk assignments to required subtype fields need a non-null literal or named parameter. Other assignment expressions are rejected for those fields. Literal and bound null assignments to primitive Java fields are rejected on both runtimes. Primitive assignments also reject expressions that may return null. Use COALESCE(expression, 0) to supply a non-null fallback for nullable numeric fields, division by a variable or zero, and nullable aggregate results. A scalar aggregate without GROUP BY or HAVING can be assigned directly when its result is non-null, such as COUNT or COALESCE(SUM(…​), 0). Parameters needed to keep an expression non-null are checked before execution. Field-to-field assignments require matching enum types and converter mappings, including fields inside expressions. Integral attributes reject real-valued assignment expressions and floating-point parameter values. Assignments also check the mapped integer range, including computed results and the narrower ranges of byte, short, and char fields. Builder predicates and JPQL parameters also check the mapped integer range. Assignments to float and Float reject finite overflow and nonzero results that would round to zero, including arithmetic expressions and nested query results. Floating-point literals that overflow or underflow double precision are rejected when the query is created.

An int or long field annotated @Version is maintained by the session. Updates and deletes compare its previous value and throw OptimisticLockException when another writer changed or deleted the row. session.increment(Entity.class, id, "counter", delta) performs database arithmetic, advances the version when present, and refreshes an already-managed instance. It returns false for a missing row or an increment outside the counter/version range. It requires a transaction and an int, Integer, long, or Long attribute without a converter, including properties holding those numeric types. Other values stored as integers, such as booleans and dates, aren’t counters. Invalid entity types, fields, or identifiers are rejected before pending changes are flushed.

Scalar field projections return the mapped Java values, including enums, dates, booleans, property values, and converter-backed objects. Integral SUM results use Long on every database, and AVG returns Double. Joined paths may end in embedded fields. LOWER, UPPER, TRIM, and ABS transform stored values. Their results and comparison operands use SQL storage values. These results can’t be assigned directly to a field with a converter. MIN, MAX, COALESCE, and NULLIF preserve compatible converter mappings. COALESCE field operands must share an enum type or converter mapping so the result can be decoded consistently. Field-to-field comparisons also require matching enum types and converter mappings. Scalar projections need a known storage type when the query is created. Bare parameters and expressions such as coalesce(:a,:b) are rejected. Association projections are also rejected; select an explicit field such as order.customer.id or select the relationship target as a root entity. Queries accept at most 999 bound parameters in total, including expanded IN lists. Larger lists are rejected before query execution. Generated key columns also have a combined limit of 3,072 bytes per key, counting up to four bytes per text character. This applies to primary keys, indexes, and collection keys, and is checked before schema creation. Explicit column type declarations remain the application’s schema contract. MySQL and MariaDB reject default binary primary keys and secondary indexes before schema creation; these keys need an explicit bounded column type declaration.

JPQL arithmetic requires numeric operands. Parameters in integral expressions must be integer values; use a real operand such as 1.0 for fractional arithmetic. Division of integral operands truncates toward zero on every database. Division and remainder by zero return null. Remainder accepts integral operands only. Integral addition, subtraction, multiplication, division, and negation fail if the result exceeds the signed 64-bit range, including in bulk assignments. Aggregate queries require each non-aggregate field to be covered by GROUP BY, including fields in HAVING and ORDER BY and outer fields referenced inside correlated nested queries. LENGTH counts characters on every backend.

28.3.3. Values and callbacks

@MappedSuperclass contributes public inherited fields to a concrete entity. @Embedded flattens a public @Embeddable value into columns prefixed with the embedding field name. An all-null embedded value reads as null. Embedded fields can be used in query paths. Enums are stored by name. Mapped query parameters and literals accept the declared enum type, a valid constant name, or null. Invalid names and constants from other enum types are rejected before execution. Fluent and JPQL predicates require Boolean inputs for ordinary Boolean fields; fields with converters accept their declared domain values. Boolean values use numeric storage; native BOOL and BOOLEAN column declarations are rejected.

Public no-argument void methods can receive @PrePersist, @PostPersist, @PreUpdate, @PostUpdate, @PreRemove, @PostRemove, and @PostLoad callbacks. Entities with lifecycle callbacks require openSession(), even without relationships or version fields. Relationships added by @PrePersist, @PostPersist, or @PreUpdate follow PERSIST cascades in the current flush, including newly created collections. Flush rechecks entries changed by later callbacks, including newly inserted rows, until the managed state matches the database. If callbacks keep changing state for 1,000 passes, flush fails and the transaction requires rollback.

Inherited callbacks run before subclass callbacks; overriding a callback method replaces the inherited declaration. Client scalar Property fields participate in dirty checking and counters. Backend entities use plain fields because the backend runtime doesn’t contain the client properties package.

@Convert(converter=CodeConverter.class, storageType=String.class) maps a custom basic value to a SQL scalar. The converter implements com.codename1.orm.session.AttributeConverter<Code,String> and has a public no-argument constructor. It receives null values too, including mapped JPQL NULL literals. Query-builder predicates and JPQL comparison parameters and literals apply the field’s converter. The builder converts values before choosing equality or SQL null predicates. JPQL and builder IS NULL tests also apply the mapped null conversion. A converter that stores null as a non-null sentinel uses a comparison with that sentinel. Values must match the converter’s input type; use a named parameter for custom objects. Identifiers and versions can’t use converters. This includes every component of an embedded identifier.

28.3.4. Identity and inheritance

Keep @Id for identity-column generation. Managed mappings require int, long, Integer, or Long identity fields; client IntProperty and LongProperty fields are supported too. Use @Id(autoIncrement=false) for assigned keys. @GeneratedValue selects UUID (for String IDs), SEQUENCE, or TABLE (for int/long IDs). PostgreSQL uses native sequences; other engines use a transactional generator table. UUIDs are version 4. Identity IDs become available on flush; the other strategies allocate on persist. Production migrations must provision the selected generators. Mappings with a cycle of required owning to-one links and identity-generated keys are rejected during processing: the first insert can’t supply every required key. SQLite and PostgreSQL support required cycles with keys assigned before insertion because their foreign keys are deferred until commit. MySQL and MariaDB enforce foreign keys immediately, so sessions reject required cycles before creating tables or writing data, even when the keys are known.

Identifier components are normalized and checked against their storage types before a cache lookup or SQL query, including the mapped integer range. Incompatible values raise IllegalArgumentException.

For composite IDs, use several @Id(autoIncrement=false) fields or one @EmbeddedId whose type is @Embeddable. Lookup accepts Identifier.of(component1, component2) in declared-field order; an embedded-ID mapping also accepts its key object. Relationships and join tables include all key components. Generated composite IDs are rejected.

@Inheritance on an entity root enables single-table inheritance, including abstract roots and polymorphic queries. @DiscriminatorValue("customer") assigns a stable discriminator to a concrete subtype. A base-class lookup and a subclass lookup return the same managed instance for the same row. A subtype query can’t address fields or relationships declared only by a sibling subtype. The physical table still includes the complete hierarchy for row loading. Use explicit discriminator values when class names may change. Every hierarchy member uses the root’s identifier generator, including its default generator name.

28.3.5. Lists, maps, and owned values

Owning Lists store positions in a list_position join-table column by default, so repeated many-to-many links and reordering survive reload. @OrderColumn can name that column explicitly. @OrderBy("name ASC, id DESC") orders an entity collection by target fields at load time; the position column still keeps repeated links distinct. An inverse mappedBy collection can use @OrderBy.

For an entity map, @MapKey(name="code") derives keys from a basic field on the target entity. The map key type must match that field’s Java type before conversion, using the boxed type for a primitive field. Keys must be non-null and unique in that collection. When changing the target key field, update the Java map keys as well.

@ElementCollection stores owned scalar values in a separate table. It supports List, Set, and Map with String keys. Values may be String, boxed primitive types, or java.util.Date. Lists preserve order and duplicate values. Map columns can be named with @MapKeyColumn; @JoinTable names the collection table. Element-map keys accept at most 255 Unicode code points on every database. query.containsElement("tags", "important") tests membership without loading the collection. Changing a mutable Date value is detected during flush.

28.3.6. Schema and locking

Run session.createTables() outside application transactions for development schemas. It creates missing entity/collection tables, primary and foreign keys, one-to-one uniqueness, and relationship indexes. @Column(unique=true) and @Entity(indexes=@Index(name="by_name", fields={"name"})) declare additional indexes. Generated text keys, indexed fields, and foreign keys accept at most 255 Unicode code points during managed inserts, updates, and bulk assignments on every database. Specify @Column(type=…​) to control the SQL type and its value limits yourself.

session.validateSchema() checks mapped entity columns, storage-type families, whether null values are allowed, and primary-key membership in entity and collection tables. It doesn’t migrate schemas or validate every database-specific constraint. Existing tables are left to explicit migrations. In particular, a legacy client DAO’s text char columns need migration to the managed layer’s numeric UTF-16 storage when moving to a managed schema.

Backend sessions support find(type, id, LockMode.PESSIMISTIC_READ) and PESSIMISTIC_WRITE, or lock(entity, mode), inside a transaction. PostgreSQL and MySQL/MariaDB use their row-lock clauses. SQLite rejects these modes explicitly; it can’t provide row-level pessimistic locks. Invalid or unsupported lock modes are rejected before pending changes are flushed.

28.3.7. Supported boundary

This API provides managed persistence through CN1 annotations; it’s not a JPA provider or a Hibernate binary replacement. Entity access uses public fields. Getter/setter access mappings, property-backed associations, joined/table-per-class inheritance, embeddable element collections, and independently stored entity-map keys aren’t supported. Nested fetch graphs and mixed entity/scalar JPQL projections are also outside the current query subset. Use the builder’s count() for distinct composite identities. Native device execution and each backend database version still require application integration testing.

28.4. Validation

The annotation processor fails the build when:

  • @Entity lands on an interface, or an abstract entity has no @Inheritance root.

  • A concrete entity has no public no-arg constructor.

  • No identifier is mapped, or a composite identifier requests generated values.

  • Persistent column/table/index names conflict, or mappedBy doesn’t resolve.

  • A field’s static type or relationship mapping is unsupported.

Errors are accumulated so the first build run reports every offending entity at once.

28.5. How the plumbing works

cn1:process-annotations writes one <SimpleName>Cn1Dao per @Entity in the source class’s package (com.example.UserCn1Dao next to com.example.User), plus a single cn1app.DaoBootstrap whose constructor calls UserCn1Dao.register(), OrderCn1Dao.register(), …​ for every accepted @Entity class. At app start:

  • On iOS / Android the build server probes the project zip for cn1app/DaoBootstrap.class and splices new cn1app.DaoBootstrap(); into the per-build application stub before Display.init. ParparVM rename and R8 obfuscation rewrite the call site and the generated dao together, so the direct symbol reference stays valid after the pass.

  • On the JavaSE simulator and desktop run JavaSEPort#postInit loads the bootstrap via Class.forName("cn1app.DaoBootstrap"). Class-loading is the legitimate path here — JavaSE runs unobfuscated.

Projects with no @Entity classes produce no bootstrap; the build server probe falls through and the registry stays empty.

The runtime registry is keyed on Class#getName(); obfuscation renames the call sites and the registered keys together within a single execution. The keys are never persisted across builds, so the renaming has no observable effect on behavior.

29. Media and Audio

29.1. MediaManager and MediaPlayer

MediaPlayer is a peer component, understanding this is crucial if your application depends on such a component. You can learn about peer components and their issues here.

The MediaPlayer allows you to control video playback. To use the MediaPlayer you need to first load the Media object from the MediaManager.

The MediaManager is the core class responsible for media interaction in Codename One.

You should also check out the Capture class for things that aren’t covered by the MediaManager.

In the demo code below you use the gallery functionality to pick a video from the device’s video gallery:

final Form hi = new Form("MediaPlayer", new BorderLayout());
hi.setToolbar(new Toolbar());
Style s = UIManager.getInstance().getComponentStyle("Title");
FontImage icon = FontImage.createMaterial(FontImage.MATERIAL_VIDEO_LIBRARY, s);
hi.getToolbar().addCommandToRightBar("", icon, (evt) -> {
    Display.getInstance().openGallery((e) -> {
        if(e != null && e.getSource() != null) {
            String file = (String)e.getSource();
            try {
                Media video = MediaManager.createMedia(file, true);
                hi.removeAll();
                hi.add(BorderLayout.CENTER, new MediaPlayer(video));
                hi.revalidate();
            } catch(IOException err) {
                Log.e(err);
            }
        }
    }, Display.GALLERY_VIDEO);
});
hi.show();
Video playback running on the simulator
Figure 243. Video playback running on the simulator
Video playback running on an Android device. Notice the native playback controls that appear when the video is tapped
Figure 244. Video playback running on an Android device. Notice the native playback controls that appear when the video is tapped
Video playback in the simulator will work with JavaFX enabled. This is the default for Java 8 or newer so recommend using that.

29.2. PCM Audio Buffers, Mixing, and Effects

AudioBuffer stores raw interleaved float PCM samples in the [-1, 1] range, and WAVWriter writes those samples to a WAV file.

The portable live PCM source in Codename One is microphone capture: create an AudioBuffer with MediaManager.getAudioBuffer(…​), then create a recorder with MediaRecorderBuilder.redirectToAudioBuffer(true). The builder’s path value is the buffer key in this mode; it isn’t a file path.

String bufferKey = "voice-buffer";
AudioBuffer livePcm = MediaManager.getAudioBuffer(bufferKey, true, 8192);
livePcm.addCallback(buffer -> {
    float[] frame = new float[buffer.getSize()];
    buffer.copyTo(frame);
    // Process or copy this frame before the next callback arrives.
});

Media recorder = MediaManager.createMediaRecorder(
        new MediaRecorderBuilder()
                .path(bufferKey)
                .samplingRate(44100)
                .audioChannels(1)
                .redirectToAudioBuffer(true));
recorder.play();

// Later:
recorder.cleanup();
MediaManager.releaseAudioBuffer(bufferKey);

When the VideoIO API is available, it can also produce PCM from a prerecorded video file. VideoIO.getVideoIO().openReader(path).readAudio() returns the decoded audio track as an AudioBuffer, so video audio can be mixed or processed with the same APIs shown below. VideoIO support is platform-gated with VideoIO.isSupported(), and JavaScript currently reports no decoded audio track even though frame decoding and encoding are supported.

Codename One still doesn’t expose a standalone portable decoder that converts an arbitrary prerecorded audio-only file, such as MP3 or AAC, into an AudioBuffer without going through a video container API. Use the normal Media APIs to play those files. If you need offline PCM editing for audio-only formats, decode them before passing samples to Codename One, or add a platform decoder API for that format.

When you need to combine PCM clips before writing or playing them, use AudioMixer. It creates a sample-accurate timeline on one sample clock. Each track must use the same sample rate and channel count as the mixer. Track offsets are supplied in milliseconds and are rounded to the nearest sample frame on the mixer clock; if you already know the exact frame position, use addTrackAtFrame(…​).

The mixed buffer is clipped to the normalized PCM range [-1, 1], so it can be passed directly to WAVWriter. The mixer doesn’t resample or convert channel layouts; downsample or otherwise convert source buffers before adding them if they’re not already on the target clock.

AudioEffects provides small platform-neutral PCM transforms that compose with the mixer:

AudioBuffer voice = AudioEffects.normalize(livePcm, 0.9f);
AudioBuffer brighter = AudioEffects.equalize(voice,
        0.8f,  // low band gain
        1.0f,  // mid band gain
        1.25f  // high band gain
);

AudioBuffer karaoke = AudioEffects.removeCenter(stereoMusic);
AudioBuffer centeredSpeech = AudioEffects.isolateCenter(stereoInterview);
AudioBuffer voiceEnhanced = AudioEffects.midSide(stereoInterview, 1.4f, 0.6f);

equalize(…​) is a simple three-band tone shaper for lightweight cleanup and demos. removeCenter(…​), isolateCenter(…​), and midSide(…​) use classic stereo mid/side processing. They can reduce, isolate, or enhance centered dialog only when the source is stereo and the voice is actually centered. These methods aren’t source-separation, speech-isolation, or noise-reduction APIs.

AudioMixer and the simple buffer-wide AudioEffects operations use the com.codename1.util.Simd float-array API internally when the current platform supports it. Stateful filters, such as the equalizer, stay scalar because each sample depends on the previous filter state. You can force scalar behavior for diagnostics with AudioMixer.setSimdOptimizationsEnabled(false) or AudioEffects.setSimdOptimizationsEnabled(false).

30. Video encoding & decoding (VideoIO)

The VideoIO API is to video what the ImageIO API is to still images, but it goes deeper. It uses the platform’s native codecs to:

  1. Encode application rendered frames and audio into a standard video file, with deep control over dimensions, frame rate, codec, bitrate, key frame interval and the audio track.

  2. Decode an existing clip into frame accurate RGBA frames and PCM audio.

The decode side is something the Media player API doesn’t provide. Media is a player: its setTime() snaps to key frames and gives no exact frame guarantee. VideoReader, by contrast, decodes the precise frame at a requested time, which is what you need when you want to read an imported clip (for example a screen recording) frame by frame.

VideoIO is supported on iOS, macOS, Android, Windows, Linux, JavaScript and the desktop simulator. It’s not available on the TV, Watch or Car targets.

30.1. Checking for support

As with other optional platform features, always gate usage with isSupported():

if (!VideoIO.isSupported()) {
    // Video encoding / decoding is not available on this platform
    // (for example TV, Watch or Car), so fall back gracefully.
    return;
}
VideoIO io = VideoIO.getVideoIO();
On the desktop simulator VideoIO is backed by an ffmpeg/ffprobe binary. The simulator bundles one automatically; if isSupported() returns false on the desktop, make sure ffmpeg is on your PATH or set the ffmpeg.dir system property.

30.2. Encoding a video

Build a VideoWriter with a VideoWriterBuilder, then push frames and (optionally) audio. Because every frame is an ordinary Image that you draw into, you have complete control over the pixels - render charts, overlays, transformed camera frames, generated animation, anything.

String out = FileSystemStorage.getInstance().getAppHomePath() + "/generated.mp4";
int w = 640, h = 480;
float fps = 30;

VideoWriter writer = new VideoWriterBuilder()
        .path(out)
        .width(w).height(h).frameRate(fps)
        .videoCodec(VideoIO.CODEC_H264).videoBitRate(4_000_000)
        .build();

// Each frame is just an Image you fully control: draw whatever you like.
for (int i = 0; i < 90; i++) {                 // 3 seconds at 30fps
    Image frame = Image.createImage(w, h, 0xff000000);
    Graphics g = frame.getGraphics();
    g.setColor(0xffffff);
    g.fillRect((i * 8) % w, h / 2 - 20, 60, 40);
    writer.writeFrame(frame, Math.round(i * 1000f / fps));
}
writer.close();

To add an audio track, enable it on the builder (.hasAudio(true)) and push interleaved 16 bit PCM with VideoWriter.writeAudio(…​), or hand it an AudioBuffer.

30.3. Decoding a video frame by frame

VideoReader exposes the clip’s duration, dimensions and frame rate, a frame accurate frameAt(long) and a variable-to-constant frame rate resampler readFrames(fps, callback) that walks the whole clip emitting evenly spaced frames:

VideoReader reader = VideoIO.getVideoIO().openReader(videoPath);
System.out.println(reader.getWidth() + "x" + reader.getHeight()
        + " " + reader.getFrameRate() + "fps, "
        + reader.getDurationMillis() + "ms");

List<Image> thumbnails = new ArrayList<>();

// Frame accurate single frame (unlike Media.setTime which snaps to key frames):
VideoFrame oneSecond = reader.frameAt(1000);
if (oneSecond != null) {
    thumbnails.add(oneSecond.toImage());
}

// Resample the (possibly variable frame rate) clip to a constant 10fps stream:
reader.readFrames(10, f -> {
    // f.getARGB() / f.toImage() give you the decoded RGBA pixels
    thumbnails.add(f.toImage());
    return thumbnails.size() < 50;   // stop after 50 frames
});
reader.close();

The audio track, when present, is decoded to interleaved PCM:

VideoReader reader = VideoIO.getVideoIO().openReader(videoPath);
if (reader.hasAudio()) {
    AudioBuffer pcm = reader.readAudio();
    System.out.println("Decoded " + pcm.getSize() + " PCM samples at "
            + reader.getAudioSampleRate() + "Hz, "
            + reader.getAudioChannels() + " channels");
}
reader.close();

30.4. Discovering available codecs

The set of codecs is device dependent. Enumerate it to pick the best one (and to detect hardware accelerated encoders):

VideoIO io = VideoIO.getVideoIO();
for (VideoCodec codec : io.getAvailableEncoders()) {
    System.out.println(codec.getId()
            + " (" + codec.getName() + ")"
            + (codec.isHardwareAccelerated() ? " [hardware]" : ""));
}
boolean canEncodeH264 = io.isEncoderSupported(VideoIO.CODEC_H264);

30.5. Platform support

PlatformBackend

iOS / macOS

AVFoundation (AVAssetReader / AVAssetWriter, hardware H.264/HEVC)

Android

MediaCodec + MediaMuxer / MediaExtractor + MediaMetadataRetriever

JavaScript

HTML5 <video> + <canvas> decode + WebCodecs encode

Windows

Media Foundation (IMFSourceReader / IMFSinkWriter)

Linux

GStreamer (appsrc / appsink)

Simulator

ffmpeg / ffprobe

TV / Watch / Car

Not supported (isSupported() returns false)

31. Video capture constraints API

The new video capture constraints API allows you to specify "constraints" when capturing videos. Constraints include:

  1. Video Quality (High or Low)

  2. Maximum duration

  3. Most file size

  4. Video Resolution (that is, width and height).

Support for these constraints vary by platform and device, but the API allows you to check if your constraints are supported at runtime. Essentially, you set your "preferred" constraints, and the API will give you its best try at meeting those constraints. This is like setting a visual Component’s preferred width and height. The layout manager takes these preferred dimensions under advisement, but sets the size on its own.

31.1. Example 1: Capturing a Low-Quality 5-Second clip

Suppose you want to allow the user to capture a short (5 second) clip, in a low resolution, appropriate for sharing on a social media platform. You create your VideoCaptureConstraints object as follows:

// The original wrote "new VideoCaptureConstraint()" -- a class that does
// not exist. Corrected while restoring it: a snippet that cannot compile is
// worse than no snippet.
VideoCaptureConstraints cnst = new VideoCaptureConstraints()
    .preferredQuality(VideoCaptureConstraints.QUALITY_LOW)
    .preferredMaxLength(5);

This constraint can then be passed to Capture.captureVideo() to get the captured file:

String videoPath = Capture.captureVideo(cnst);

31.2. Not all platforms support all constraints

How do you know whether your constraints will be honored? When the platform doesn’t support the maximum length, you may want to take a different path. Ask the constraint object whether it’s supported.

For example:

if (cnst.isMaxLengthSupported()) {
    // The max length constraint we specified is supported on this platform.
} else {
    // It is not. Check the effective value to see whether it is partially
    // supported.
    int effectiveMaxLength = cnst.getMaxLength();
    if (effectiveMaxLength == 0) {
        // Not supported at all: the user can capture without a duration limit.
    } else {
        // Set to a different value than preferredMaxLength asked for, so the
        // platform is at least trying to accommodate us.
    }
}

You can probe a constraint to see whether the entire constraint is supported (that is, whether it will be obeyed), or whether any particular aspect of it will be supported using the following methods:

  • isSupported() - True if all preferred constraints are supported.

  • isQualitySupported() - True if the preferred quality setting is supported.

  • isMaxLengthSupported() - True if the max length setting is supported.

  • isMaxFileSizeSupported() - True if the max file size setting is supported.

  • isSizeSupported() - True if the specified preferred width and height constraints are supported.

31.3. Example 2: Specifying explicit width and height

Suppose you want to capture a video with resolution 320×240. You would begin with this constraint:

VideoCaptureConstraints cnst = new VideoCaptureConstraints()
    .preferredWidth(320)
    .preferredHeight(240);

Explicit width and height constraints aren’t well-supported across platforms. Android doesn’t support them at all. iOS supports 3 specific sizes. JavaScript supports it when running on a desktop browser or on Android - but not on iOS. Etc.

Find out if this constraint will be obeyed:

if (cnst.isSizeSupported()) {
    // This platform supports the constraint, so the captured video will be
    // exactly 320x240.
} else {
    // Not supported -- see whether the platform will get close.
    int effectiveWidth = cnst.getWidth();
    int effectiveHeight = cnst.getHeight();
    int quality = cnst.getQuality();
    if (effectiveWidth == 0 && effectiveHeight == 0) {
        // No control over width and height. In many cases the platform will
        // approximate with the quality setting instead.
        if (quality != 0) {
            // Quality was set for us. Since 320x240 is small, it would
            // probably be QUALITY_LOW.
        }
    } else {
        // Could not capture at 320x240, but supplied the closest size it can.
    }
}

31.4. Constraint support by platform

PlatformSizeQualityMax LengthMax File Size

iOS

Limited

Yes

Yes

No

Android

No*

Yes

Yes

Yes

Simulator/Java SE

No

No

No

No

JavaScript (Desktop)

Yes

Yes

Yes

No

JavaScript (Android)

Yes

Yes

Yes

No

JavaScript (iOS)

No

No

No

No

* If size is specified, the platform will try to translate to the appropriate quality constraint.

Server side

32. Server-side backend

The backend compiles a Java HTTP handler into a native executable. It uses the same ParparVM pipeline as the iOS, Windows and Linux ports: javac produces bytecode, ParparVM turns that into C, and clang compiles the C together with the runtime into one binary. Nothing interprets anything at run time, and there is no JVM underneath.

The measurements in this chapter come from vm/backend/benchmarks on two pinned cores against 64 connections, and the harness is in the repository so you can disagree with them.

Java compiled to C to a native binary, and a request travelling through a host thread and a virtual thread to the handler
Figure 245. The build pipeline and the request path

32.1. What this doesn’t replace

Spring Boot, Quarkus, Micronaut and Jakarta EE aren’t the competition here. They carry a security stack, a starter for every service a company runs, a container that discovers beans in any jar on the class path, and two decades of operational knowledge. If you are running a Spring service today and it works, these chapters aren’t asking you to move it.

What this runtime borrows from them is the programming model. Controllers, services, dependency injection, declarative transactions, scheduled jobs and managed beans use Spring’s annotation names and mean what they mean there, so a Spring developer can read a Codename One backend without learning it first. What it doesn’t borrow is the container: every one of those annotations is resolved while the project builds, into ordinary code, and there is nothing left to interpret when the server runs.

What those frameworks assume is a JVM, and that paying for one is reasonable. For most server work that assumption holds. This exists for the work where it fails.

32.2. Why it exists

A JVM charges you twice. It charges start-up on every cold process, and it charges a baseline heap for as long as the process lives. Both are fine when a service runs for weeks and handles millions of requests. Neither is fine when the process exits after 200 milliseconds, when you are billed per invocation, or when the instance is capped at 128 MB.

That describes a specific and growing slice of server work: serverless functions, sidecars, edge workers, webhook receivers, small always-on services. Java is thin on the ground there, and the reason is arithmetic rather than taste. Teams therefore reach for Go or Node, and a Java shop that does this ends up with two languages, two toolchains, and two definitions of every object that crosses the wire.

The point of the backend is to remove the reason to leave Java for that slice. It doesn’t try to take work the JVM already does well.

32.3. How the backend chapters fit together

This chapter covers what every server needs: the module, the build, the concurrency model and configuration. The chapters after it each take one part of a server and go into depth:

32.4. A first server

The archetype (with -DprojectType=app-with-backend) and the initializr (with the App with backend project type) both generate a backend module beside the app:

myapp/
  common/    shared app code
  backend/   the server
    pom.xml
    src/main/java/com/example/myapp/Notes.java

A server-only project, generated with -DprojectType=backend-only or the initializr’s Backend only type, has no common module: the server is the whole project, at the root (see Section A.1.2, “Backend-only projects”).

A server is a class with routes on it. The annotations are Spring’s, under Codename One’s package names, so this reads the same way to anyone who has written a Spring controller:

@RestController
@RequestMapping("/notes")
public class Notes {
    private final Map<Long, Map> store =
            Collections.synchronizedMap(new LinkedHashMap<Long, Map>());
    private final AtomicLong nextId = new AtomicLong(1);

    @GetMapping("/healthz")
    public String health() {
        return "ok";
    }

    @GetMapping("/{id}")
    public Map read(@PathVariable("id") long id) {
        return store.get(Long.valueOf(id));      // null becomes a 404
    }

    @GetMapping
    public List list(@RequestParam(value = "limit", defaultValue = "20") int limit) {
        List page = new ArrayList();
        synchronized (store) {                   // iterating needs the map's own lock
            for (Map note : store.values()) {
                if (page.size() >= limit) {
                    break;
                }
                page.add(note);
            }
        }
        return page;
    }

    @PostMapping
    @ResponseStatus(201)
    public Map create(@RequestBody Map note) {
        long id = nextId.getAndIncrement();
        Map stored = new LinkedHashMap(note);
        stored.put("id", Long.valueOf(id));
        store.put(Long.valueOf(id), stored);
        return stored;
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(204)
    public void delete(@PathVariable("id") long id) {
        store.remove(Long.valueOf(id));
    }
}

There is no main here, and that’s the point. Every server opens with the same twenty lines — start a listener, install a shutdown handler, wait on it — and getting any of them wrong produces one that leaks connections on SIGTERM, or one that ends the moment main returns and says nothing about why. The build writes those lines, from the controllers it finds.

It also writes the router. The obvious hand-written form, if ("/healthz".equals(request.getTarget())), builds a String for the target, hashes it and compares it — for every route it tries before the one that matches, on every request — and breaks as soon as a client appends ?probe=1. The generated router holds each route as a byte[] and asks the request whether its path bytes are those bytes, so a route with no path variables allocates nothing at all and a query string can’t break it.

The return value decides the response. A String is sent as text, anything else as JSON, null is a 404, and @ResponseStatus sets the code for the cases where 200 isn’t it. A handler that needs something this doesn’t model takes the HttpServer.Request itself and answers exactly as it would have before.

Two commands matter:

# the property is not optional: the backend module lives in a profile, and
# without it Maven cannot see it in the reactor at all
mvn -pl backend -Dcodename1.platform=backend cn1:backend           # run it on this JVM
mvn -pl backend -Dcodename1.platform=backend cn1:backend-package   # build the native binary

In a backend-only Maven project, run ./mvnw cn1:backend and ./mvnw cn1:backend-package from the root, with no module or property.

In a Gradle project the backend is the backend subproject (or the whole project, for a backend-only application), and the same two steps are tasks:

./gradlew :backend:runBackend       # run it on this JVM
./gradlew :backend:backendPackage   # build the native binary

Drop the :backend: prefix in a backend-only project. Section 3.8, “The backend” covers adding a backend to an existing Gradle application.

The first is the development loop. Both run the same protocol code — there is one copy of HttpServer, and only the layer underneath it differs — so behaviour can’t drift between the loop you develop in and the binary you ship. The local run doesn’t terminate TLS, by design, and therefore doesn’t serve HTTP/2: both refuse with a message that says so, because a second TLS implementation would have its own bugs rather than production’s.

32.5. What the build writes

Every annotation in these chapters is read from the compiled classes, after javac and before the translator, during the process-classes phase. The build works out everything the annotations ask for and writes it down as plain Java, so the translator and the dead-code pass see an ordinary program:

Annotated sources compiled by javac, resolved by the build into generated wiring, routers and aspect classes, then translated
Figure 246. What the build does with a backend module
  • BackendApplication is the main. It builds the configuration, opens the database, starts the server and installs the shutdown handler.

  • BackendWiring is the whole of dependency injection: a new for each bean in dependency order, the calls that set its injected fields, and its @PostConstruct method. Chapter 34, Backend beans and dependency injection describes what goes into it.

  • A Router per controller holds each route as a byte[] and binds each parameter with code written for its type.

  • A Cn1Aspects class per annotated class carries what @Transactional, @Async, @Timed and @Counted add around a method. The method keeps its name and signature, its body moves to a method of its own, and the method calls the aspect around that body.

That has three consequences worth knowing from the start. A dependency that can’t be satisfied, a route claimed twice and a cron expression with a typo are build errors, each naming the class and member at fault, rather than failures the first request finds. Starting a server with fifty beans costs what the fifty constructors cost, because there is no scan and no lookup. And a feature the application skips isn’t in the binary at all: the translator drops every class nothing references, and the generated code references only what the annotations asked for.

The generated classes land in target/classes beside the module’s own. To see what the build decided about the beans without reading bytecode, ask a running development server: the backend_beans tool described in Chapter 40, Backend MCP and the agent loop lists every bean, its scope and what was injected into it.

A process runs one backend. Starting a second while the first is running fails with an error; starting again after it has stopped is fine. To scale out, run more processes: keep sessions in the database store and give scheduled jobs a lock, so the instances share them.

32.6. Virtual threads and the request loop

The concurrency model is the part most worth understanding, because it’s what makes a handler that blocks acceptable.

Every connection gets a virtual thread. Not a pooled worker that a connection borrows for one request — a stack that belongs to that connection until it closes. The handler can block on a socket read, a database round trip or a file, and it costs a parked stack rather than an OS thread.

Host threads run those virtual threads, and there is one host per core. A descriptor is registered in exactly one host’s epoll set, so it can only ever be reported to that host, and only that host ever touches its virtual thread. That affinity is why the scheduler needs no locks on the hot path.

The loop is: the host polls, a descriptor becomes readable, the host resumes that connection’s virtual thread, the handler runs until it needs bytes that haven’t arrived, and it parks. Parking returns control to the host, which polls again. When the handler finishes a response the descriptor stays armed, so the next request on that connection costs no system call to set up.

These are ParparVM’s own virtual threads, a feature of the translated runtime, and they aren’t the virtual threads of Java 21 and later. The two share a name and little else. A Java virtual thread unmounts whenever the JDK blocks on its behalf. One of these switches stacks in the translated C code and parks only where the runtime has made parking possible: waiting on a socket. That covers the connection it serves and every outbound one — a PostgreSQL or MySQL query, a Web call, a TLS handshake — so a handler waiting on another service costs a parked stack, and the host serves other connections meanwhile. Anything else that blocks holds the host thread under it and every virtual thread that host runs: SQLite and file access, which are local calls rather than sockets, resolving a host name, and Object.wait(). They’re also pinned: a virtual thread stays on one host for its whole life.

That’s why the development run on the JVM doesn’t use Java’s virtual threads to stand in for them, even for debugging. It serves connections on a pool of platform threads and runs VIRTUAL work on platform threads too. Code that’s correct there can still stall a host on the native build, so test anything that depends on how threads interleave — a handler that waits, a task that blocks — on the native server.

Host count follows cores rather than the workers argument. On two pinned cores, 16 hosts served 117 requests where 2 hosts served 257,297: past one host per core they compete for the cores the server needs. In this mode workers stops meaning "requests in flight," because the virtual threads supply that.

32.7. The life of a request

A server’s handlers are arranged in a chain, and each one either answers a request or passes it on by answering null. The order is fixed, so which handler wins never depends on registration order or on what happens to be in a directory:

A request passing through the MCP endpoint, trace relay, management endpoints, the application’s routes and static files, inside a wrapper that records metrics and saves the session
Figure 247. The life of one request
  1. A WebSocket upgrade is matched first, against the @WebSocketMapping routes, and never reaches the chain.

  2. The server’s own endpoints come next, each only when it’s turned on: MCP at /mcp, the trace relay at /otel/v1/traces, and the management endpoints under /manage. They come before the application’s routes so a catch-all route can’t answer a health check.

  3. Then the application’s routes.

  4. Static files from cn1.static.root are always last, so a file can’t shadow a route by being named like one.

  5. A request nothing answered is a 404.

Around the chain sits one wrapper, and it’s where the per-request features live. It starts the clock for the request duration metric, makes the request available to request-scoped beans, and records the request for the development tools. After the chain answers it saves the session and adds its cookie, destroys the request’s request-scoped beans, and records the duration under the route that matched. A handler that throws is answered with a 500 whose body says only internal error, and the exception goes to the log rather than to the client.

32.8. Configuration and profiles

A server runs against a file on a laptop and a managed database in production, and the difference between those two can’t live in the source. It lives in properties files beside the binary and in the environment:

application.properties, committed:

cn1.datasource.url=${DATABASE_URL}
cn1.server.port=8080

application-dev.properties, also committed:

cn1.datasource.url=:memory:
CN1_PROFILE=dev ./server                         # SQLite, nothing installed
DATABASE_URL=postgres://app:secret@db/app ./server   # production

A key is resolved in one order, and the first layer that has it wins: a system property of that exact name, then the environment variable it maps to (cn1.datasource.url becomes CN1_DATASOURCE_URL), then the environment variable a platform already sets for it, then application-<profile>.properties, then application.properties.

The third layer is worth spelling out. PORT and DATABASE_URL are set for you by every platform-as-a-service worth the name, and a server that ignored them would need a wrapper script to start at all, so they’re read where those two keys are read.

A value may name an environment variable as ${NAME} or ${NAME:fallback}, and that reference resolves when the value is READ rather than when the file loads. That’s what lets the committed application.properties above name a variable only production sets: the development profile overrides the key, so the unset variable is never read. A reference that’s read and can’t be resolved fails with a message naming it, because the alternative is a server that opens a SQLite file called ${DATABASE_URL} and finds it empty.

Nothing here is required. A binary with no properties file beside it reads its whole configuration from the environment, which is the normal shape for a container built FROM scratch: there is no file next to the binary because there is nothing next to the binary.

KeyWhat it sets

cn1.profile

The active profile. dev, development, test and local are development profiles, which changes two defaults and nothing else.

cn1.datasource.url

A SQLite path, or a postgres:// or mysql:// URL. Also read from DATABASE_URL. On a development profile an unset value means an in-memory SQLite database; on any other profile it’s an error, because a service that writes into a database inside its own process, with nothing saying so, loses everything at the next deploy.

cn1.datasource.pool.size

How many connections to hold. The default is eight for a server engine, four for a SQLite file and one for an in-memory database.

cn1.server.port

The port. Also read from PORT.

cn1.server.address

The address to listen on. Every interface by default; 127.0.0.1 keeps the server reachable only from the machine it runs on.

cn1.server.workers

The size of the request thread pool, 16 by default. Under virtual threads it no longer bounds the requests in flight; see Section 32.6, “Virtual threads and the request loop”.

cn1.server.tls.certificate, cn1.server.tls.key

A PEM certificate and key to terminate TLS with. One without the other is refused rather than serving plaintext under a name that promises otherwise.

cn1.static.root

A directory to serve files from, after every route.

cn1.orm.createTables

Whether the generated daos create their tables at start-up. True on a development profile, false everywhere else.

cn1.config.location

The directory the properties files are read from; the working directory by default. Read only from a system property or the environment, since it decides where the files are.

cn1.server.backlog

The listen backlog, 512 by default.

cn1.server.shutdownTimeoutMillis

How long a stop waits for the requests in flight, 10 seconds by default. Zero means don’t wait.

cn1.server.tls.http2

Whether a TLS server offers HTTP/2 through ALPN. True by default.

cn1.static.prefix, cn1.static.index, cn1.static.cacheControl

Where static files are served (/static), the file a directory answers with (index.html), and their Cache-Control header (public, max-age=3600).

cn1.datasource.pool.borrowTimeoutMillis

How long a request waits for a free connection before it fails, 10 seconds by default.

cn1.datasource.busyTimeoutMillis

How long SQLite waits on a locked database file, 5 seconds by default.

One limit is read only from the environment, because it’s fixed when the server class loads: CN1_HTTP_MAX_UPLOAD_MB, 64 by default, bounds the request bodies being received at once across all HTTP/1.1 connections. A request that would pass the limit gets a 503 rather than being buffered.

The chapters that follow add their own keys — cn1.session., cn1.task.executor., cn1.management., cn1.mcp. and cn1.otel.* — and list each one where it’s described.

32.8.1. Settings compiled in

A server that ships as one executable often has no properties file beside it, so the build compiles the module’s application.properties into the server as the bottom layer of its configuration. The binary starts with the committed settings and nothing next to it, and a properties file or the environment present at run time still changes any key without a rebuild.

Only the base file is compiled in. A profile’s file describes a different deployment, so application-dev.properties stays a file the dev profile reads rather than becoming a production binary’s defaults.

Secrets — a token, a password inside a database URL — don’t belong in a committed file. Name them as ${NAME}, which is resolved from the environment when the value is read, or leave the key to the environment entirely.

Handlers that need any of this get it from the entry point the build writes.

33. Backend controllers, contracts and WebSockets

A controller turns HTTP into method calls. This chapter covers how requests are mapped to methods and how their parts are bound to parameters, how a return value becomes a response, how the app and the server share one declaration of their API, and how a server holds WebSocket connections open. Section 32.4, “A first server” introduced the shape; this is the detail.

33.1. Mapping requests

A @RestController is a class whose methods answer requests. It’s also a bean, so its constructor receives whatever it depends on, as Chapter 34, Backend beans and dependency injection describes. Each mapped method names a verb and a path:

@RestController
@RequestMapping("/api/products")
public class Products {
    private final Map<Long, Map<String, Object>> products =
            new LinkedHashMap<Long, Map<String, Object>>();

    @GetMapping("/{id}")                                       // GET /api/products/42
    public synchronized Map<String, Object> get(@PathVariable("id") long id) {
        return products.get(Long.valueOf(id));                 // null answers 404
    }

    @GetMapping("/search")                                     // GET /api/products/search?q=mug
    public synchronized List<Map<String, Object>> search(
            @RequestParam(value = "q", required = false) String query,
            @RequestParam(value = "limit", defaultValue = "20") int limit) {
        List<Map<String, Object>> out = new ArrayList<Map<String, Object>>();
        for (Map<String, Object> p : products.values()) {
            if (out.size() < limit
                    && (query == null || String.valueOf(p.get("name")).contains(query))) {
                out.add(p);
            }
        }
        return out;
    }

    @PostMapping                                               // POST /api/products
    @ResponseStatus(201)
    public synchronized Map<String, Object> create(
            @RequestBody Map<String, Object> body,
            @RequestHeader(value = "Idempotency-Key", required = false) String key) {
        Long id = Long.valueOf(products.size() + 1);
        Map<String, Object> product = new LinkedHashMap<String, Object>(body);
        product.put("id", id);
        products.put(id, product);
        return product;
    }

    @DeleteMapping("/{id}")                                    // void answers 204
    public synchronized void delete(@PathVariable("id") long id) {
        products.remove(Long.valueOf(id));
    }

    @GetMapping("/{id}/label")
    public HttpServer.Response label(HttpServer.Request request, @PathVariable("id") long id) {
        byte[] text = ("product " + id).getBytes();
        return request.respond(200, "text/plain; charset=utf-8", text);
    }
}

@GetMapping, @PostMapping, @PutMapping, @PatchMapping and @DeleteMapping cover the usual verbs, and @RequestMapping(value = "/x", method = "OPTIONS") covers the rest. On the class, @RequestMapping is a prefix for every mapping inside it, and a mapping with no path answers on the prefix itself, as create does above. A mapping may list several paths, which is how a route keeps an old URL working.

A {name} segment matches one path segment and is bound with @PathVariable. Within one controller a literal route is tried before a route with variables that would also match it, so /api/products/search above reaches search rather than get with an id of search.

What the build refuses is a route nothing could ever reach. Two methods answering the same verb and path shape are an error, even when their variables are named differently, because a variable’s name isn’t part of what a request carries. The same is true across controllers: a variable route in one controller that would answer a literal route of another is reported, since the routers are tried in turn and the literal one would never run.

33.1.1. Binding the parts of a request

Every parameter of a mapped method says where its value comes from, with exactly one annotation:

AnnotationReadsBinds to

@PathVariable("id")

a {id} segment of the path

String, or a numeric or boolean primitive

@RequestParam("q")

a query parameter, or a field of a form body

String, or a numeric or boolean primitive

@RequestPart("file")

one part of a multipart/form-data body

HttpServer.Part, byte[], or String for a text field

@RequestHeader("Idempotency-Key")

a request header

String, or a numeric or boolean primitive

@RequestBody

the body

String, a Map or a List, or a class of your own and collections of it

none, typed HttpServer.Request

the request itself

anything the request exposes

The name is required. Java drops parameter names when it compiles unless it’s told to keep them, and a router that guessed would bind the wrong value in silence, so @RequestParam("q") names the parameter the client sends rather than the variable in the method. A parameter with no annotation, or with two, is a build error: one parameter reads from one place, which matters most for an input like a credential that could otherwise arrive from a query string when the code meant a header.

@RequestParam, @RequestHeader and @RequestBody are required unless they say otherwise. A request that leaves out a required value is answered 400, with the value’s name in the body, before the method runs. defaultValue supplies a value for one that’s missing. Two combinations are refused at build time, because each would hand the method a value nobody sent: required = false on a primitive with no defaultValue, since an int can’t hold "absent" and would arrive as 0; and a defaultValue that isn’t a value of the parameter’s type.

A body is parsed as JSON. A String, Map or List parameter gets it as parsed; a class of your own — an entity, a DTO — or a List<Order> of them is filled in by a codec the build writes for that class, as Jackson fills one in for a Spring controller. Section 33.1.3, “Your own classes as JSON” describes the form.

A request target with a malformed escape sequence, or one that isn’t valid UTF-8, is answered 400 before any route is tried, rather than as a 404 the client couldn’t explain.

33.1.2. What a method returns

Return typeResponse

void

204 with no body, or the @ResponseStatus code.

String

The text, as text/plain; charset=utf-8, with 200 or the @ResponseStatus code.

A Map, List, Set, primitive or box, or a class of your own and collections of it

JSON, with 200 or the @ResponseStatus code.

HttpServer.Response

Exactly what the method built. @ResponseStatus on such a method is a build error, since the response already carries its own status.

null, from any of the above

404.

Any other return type is a build error: a JDK class with no JSON form, such as java.io.File, would otherwise come out as the JSON string of its toString(), with the build and the request both reporting success. @ResponseStatus takes a final status between 200 and 599.

33.1.3. Your own classes as JSON

A method can return an entity or any class of your own, or take one as its body, and the build writes the JSON codec for that class and for every class its fields reach. There is no reflection at run time: the codec is ordinary code that reads and writes each field by name, so it costs what the hand-written version would.

public class Order {
    public long id;
    public String customer;
    @JsonProperty("placed_at")
    public Date placedAt;
    public List<OrderLine> lines = new ArrayList<OrderLine>();
}
public class OrderLine {
    public String sku;
    public int quantity;
}
@RestController
@RequestMapping("/orders")
public class OrdersApi {
    private final Map<Long, Order> orders =
            Collections.synchronizedMap(new LinkedHashMap<Long, Order>());
    private final AtomicLong ids = new AtomicLong();

    @PostMapping
    public Order place(@RequestBody Order order) {
        order.id = ids.incrementAndGet();
        order.placedAt = new Date();
        orders.put(Long.valueOf(order.id), order);
        return order;
    }

    @GetMapping("/{id}")
    public Order find(@PathVariable("id") long id) {
        return orders.get(Long.valueOf(id));    // null is a 404
    }
}

A POST /orders with {"customer":"Ada","lines":[{"sku":"A-1","quantity":2}]} answers with the stored order:

{"id":1,"customer":"Ada","placed_at":1740821400000,"lines":[{"sku":"A-1","quantity":2}]}

The JSON form is the one the app’s @Mapped mapper uses, so a class shared by the app and the server reads the same on both sides:

  • The fields are the class’s own and its superclasses', except static and transient ones. A public field is used directly; any other through its getX (or isX) and setX methods, and is left out when it has neither.

  • @JsonProperty("due_at") renames a field and @JsonIgnore leaves one out, both from com.codename1.annotations.

  • A Date is written as milliseconds since the epoch, which is Jackson’s default and what the app’s mapper reads, and is read from that or from an ISO-8601 date such as 2025-03-01T09:30:00Z. A byte[] is base64, an enum is its name.

  • A subclass is written with its own fields, even where a method declares the superclass.

  • A field declared Object, or a raw Map or List, is written by what it holds when the server runs. A value of a class the build writes a codec for goes through that codec; a value of any other class of yours is answered 500 rather than written as its toString(), so declare the field with its type.

  • A body’s unknown members are ignored and absent ones leave the field as the constructor set it, as a Spring Boot application does.

A body the codec can’t read — a string where a number belongs, an enum name that doesn’t exist — is answered 400 with where it was and what was expected:

$.items[2].due: expected a number or an ISO-8601 date, got true

Two objects that refer to each other, such as an order whose lines point back at it, would be written forever, so a response that nests more than 64 objects deep is answered 500 with a message naming the class; mark the field that points back @JsonIgnore. The build refuses what it can’t write a codec for, and says why: a field typed by a type variable, such as List<T> in a generic Page<T>; a body class with no constructor without arguments; an interface; and an array other than byte[].

A method that throws is answered 500 with the body internal error, and the exception is logged. The message isn’t sent to the client, because an exception message is the most common way a server leaks its internals. To answer a failure with a status and a body of your choosing, return an HttpServer.Response, built with request.respond(status, contentType, bytes) or request.respondJson(status, value).

33.1.4. Taking the request itself

A parameter typed HttpServer.Request receives the request, which is the way to anything the binding annotations don’t model: every header, the raw body, the session, or a response with headers of its own. The label method in the example above takes it for that reason.

A request’s byte arrays belong to the connection and are reused by the next request on it, so a handler that keeps the body beyond its own call copies what it needs. That’s the price of a router that allocates nothing for a route without path variables.

33.1.5. Forms and file uploads

A browser form posts its fields as application/x-www-form-urlencoded, or as multipart/form-data when it carries a file. @RequestParam reads a field of either kind of form the way it reads a query parameter, so a handler doesn’t care how the client encoded it. The query string wins when both have the name, as it does in a servlet container. @RequestPart binds one part of a multipart body, which is the way to a file:

@PostMapping("/avatars")
public Map<String, Object> upload(@RequestPart("image") HttpServer.Part image,
                                  @RequestParam("caption") String caption) {
    Map<String, Object> out = new LinkedHashMap<>();
    out.put("file", image.getFilename());
    out.put("type", image.getContentType());
    out.put("bytes", image.getSize());
    out.put("caption", caption);
    return out;
}

An HttpServer.Part has the part’s name, its file name when it’s a file, its content type, its own headers and its bytes. A body that isn’t well-formed multipart is answered 400 before the method runs, and so is a request without a required part. A handler that takes the request itself reads the same parts with getParts() and getPart(name), and reads a field with param(name).

The body limit applies to the whole multipart request, files included: 8 MB, as for any other body. A body that isn’t text — an upload, an image, application/octet-stream — reaches the handler as bytes through getBodyBytes(). getBody() returns text only for a text type, JSON, XML or a form, after checking that it’s valid UTF-8, and refuses a binary body rather than hand the handler garbled characters.

33.1.6. Compression

cn1.server.compression.enabled=true turns on gzip for responses, with the two other settings Spring Boot has for it. A response is compressed when the client sends Accept-Encoding: gzip, its type is in cn1.server.compression.mimeTypes (text, JSON, JavaScript and XML by default), and its body is at least cn1.server.compression.minResponseSize bytes (2048 by default). A static file is left unchanged, because it’s sent straight from the page cache, and so is a response whose handler set a Content-Encoding of its own. A range answer (206 or any response with Content-Range) is never compressed either, because its offsets describe the uncompressed bytes, and neither is a response with a strong ETag, which names its exact bytes, as Tomcat leaves one by default. Compression is off by default because it trades processor time for bytes, and a server behind a proxy that already compresses would pay for it twice.

A request body sent with Content-Encoding: gzip is decompressed before the handler sees it, whether responses are compressed or not. It’s held to the same 8 MB limit after decompression, so a small compressed body can’t expand past it. A request in any other encoding is answered 415.

33.1.7. Cross-origin requests

A browser refuses to let a page read a response from another origin — another scheme, host or port — unless the server says it may. A web app served from one host that calls an API on another needs the API to say so, and the cn1.cors settings do:

cn1.cors.allowedOrigins=https://app.example.com
cn1.cors.allowCredentials=true
cn1.cors.exposedHeaders=X-Request-Id
KeyWhat it sets

cn1.cors.allowedOrigins

The origins that may call the server, separated by commas, or * for any. Each is scheme://host[:port] with no path, as a browser sends it in Origin; the server refuses to start with anything else. Cross-origin requests are refused while this is unset.

cn1.cors.allowedMethods

The methods a cross-origin request may use, preflighted or not. All the common ones by default.

cn1.cors.allowedHeaders

The request headers a preflight allows, * by default.

cn1.cors.exposedHeaders

Response headers beyond the basic ones that the page may read.

cn1.cors.allowCredentials

Whether the browser sends cookies and the page may read the answer. A browser refuses this with a * origin, so the server refuses that combination when it starts.

cn1.cors.maxAgeSeconds

How long a browser may cache a preflight answer, 1800 seconds by default.

The server answers a preflight OPTIONS request itself, and adds the headers to every response to an allowed origin. A cross-origin request from an origin not on the list, or with a method not on it, is refused with a 403 before any handler runs, as Spring refuses it; a request whose Origin is the server itself isn’t a cross-origin request. A handler that maps OPTIONS or sets Access-Control-Allow-Origin itself keeps its own policy, which is how the MCP endpoint keeps the stricter one it has.

33.1.8. Serving static files

cn1.static.root names a directory, and the server answers requests under cn1.static.prefix (/static by default) with its files, after every route has had its chance. A directory is answered with its cn1.static.index file, and every file carries cn1.static.cacheControl. The resolved path must be inside the root, checked by resolving it on disk rather than by inspecting the request, so ../, an encoded escape sequence and a symbolic link out of the tree are all refused. Conditional requests and ranges are honoured, and where the platform supports it the file is sent from the page cache to the socket without passing through the process.

33.2. Sharing the contract with the app

This is where having the same language on both ends stops being a slogan. An interface annotated for the REST client generates the app’s client:

@RestClient
public interface NotesApi {
    @GET("/notes/{id}")
    void note(@Path("id") String id, OnComplete<Response<Note>> callback);

    @POST("/notes")
    void create(@Body Note note, OnComplete<Response<Note>> callback);
}

Building the backend module with -Dcn1.restServer=true generates two more types from that same interface: NotesApiServer, a synchronous interface the backend implements, and NotesApiDispatcher, which routes a method, path and body to it and binds the path and query parameters.

public class NotesEndpoint implements NotesApiServer {
    private final Map<String, Note> notes =
            Collections.synchronizedMap(new HashMap<String, Note>());

    public Note note(String id) {                // no callback: this IS the server
        return notes.get(id);
    }

    public Note create(Note note) {
        notes.put(String.valueOf(note.id), note);
        return note;
    }
}

The client’s methods are asynchronous because a UI can’t block; the server’s methods are synchronous because a handler has nothing to call back into. One declaration produces both shapes, which is what gRPC does and for the same reason.

The payoff is that changing the contract breaks the build on whichever side did not follow it, instead of producing a response the app fails to parse in the field. The data transfer objects are shared rather than transcribed, and their codecs are generated on both sides, so there is no handwritten mapping layer to drift.

The server half is off by default. Every existing project carries these interfaces for its client alone, and generating server classes into those builds would grow them for nothing.

33.3. Real-time with WebSockets

The server speaks RFC 6455, so a Codename One app can hold a live connection to a Codename One server with the same language on both ends. The client half is com.codename1.io.WebSocket, which every port has shipped for years; this is the other half of it.

An endpoint implements com.codename1.backend.WebSocket:

public static final class Echo implements WebSocket {
    public void onOpen(WebSocketSession session) throws IOException {
        session.sendText("welcome");
    }

    public void onText(WebSocketSession session, String message) throws IOException {
        session.sendText(message);
    }

    public void onBinary(WebSocketSession session, byte[] message, int offset, int length)
            throws IOException {
        session.sendBinary(message, offset, length);
    }
}

The build finds it, the way it finds a @RestController:

@WebSocketMapping("/chat")
public static final class ChatEndpoint implements WebSocket {
    public void onOpen(WebSocketSession session) { }

    public void onText(WebSocketSession session, String message) { }

    public void onBinary(WebSocketSession session, byte[] message, int offset, int length) { }
}

The annotation goes on the type rather than on a method, because a @GetMapping marks a call and a WebSocket is a connection: its contract is seven callbacks that share per-connection state, which is an object. What it keeps from @GetMapping is what a reader cares about — the path is relative to a class-level @RequestMapping, a constructor taking a DataSource or an EntityManager is injected the same way, and two endpoints claiming one path is a build error rather than something registration order decides. Write the path’s characters out: an escaped character such as %61 in the mapping is a build error too, since an upgrade is matched against the decoded path, which an escaped mapping never equals. An endpoint that injects a request- or session-scoped bean, directly or through a singleton, builds with a warning: its callbacks run outside any HTTP request, so there’s no request or session to find the bean in, and using it there throws IllegalStateException, as in Spring. Keep per-connection state in the session’s attachment instead.

Only onOpen, onText and onBinary have to be written. onPing, onPong, onClose and onError have empty defaults, and a PING is answered with its PONG before the endpoint is told, so an endpoint that ignores them still keeps its connections alive.

33.3.1. One endpoint, many connections

An endpoint is created once for the route, not once per client, which is the same shape a @RestController has. Everything belonging to one client lives on the WebSocketSession the callbacks are handed, and setAttachment is where an endpoint puts its own per-connection state.

Messages arrive whole. A client that splits a two megabyte upload into thirty-two frames produces one onBinary, and a text message is validated as UTF-8 before it is decoded, so a handler never sees a replacement character standing in for bytes the client didn’t send. The array passed to onBinary is the session’s own reassembly buffer and is valid only until that call returns — the same contract HttpServer.Request carries, and for the same reason. An endpoint that keeps the bytes copies the range it wants.

33.3.2. Sending from somewhere else

A callback runs on the thread that owns its connection, and the server reads nothing more from that connection until it returns. An endpoint may therefore block, and a slow one slows down its own client and nobody else’s.

Sending is the other way round: a session may be written to from any thread, which is what makes a broadcast possible.

public static final class Room implements WebSocket {
    // Every open session in this room. A websocket endpoint is one object shared
    // by every connection, so anything per-room lives here and anything per-client
    // lives on the session.
    private final List sessions = Collections.synchronizedList(new ArrayList());

    public void onOpen(WebSocketSession session) {
        sessions.add(session);
    }

    public void onText(WebSocketSession session, String message) {
        // Sending from a thread other than the one that owns a connection is
        // supported, and this is why: a broadcast reaches every session but one
        // from the thread that received the message.
        Object[] open = sessions.toArray();
        for(int iter = 0 ; iter < open.length ; iter++) {
            WebSocketSession other = (WebSocketSession)open[iter];
            if(other == session) {
                continue;
            }
            try {
                other.sendText(message);
            } catch (IOException err) {
                // A send fails when that peer has gone. It is already being torn
                // down; onClose will take it out of the list.
                other.abort();
            }
        }
    }

    public void onBinary(WebSocketSession session, byte[] message, int offset, int length) {
    }

    public void onClose(WebSocketSession session, int code, String reason) {
        sessions.remove(session);
    }
}

Each session serializes its own writers, so two threads can’t interleave halves of two messages on one connection. What the server doesn’t do is queue: sendText writes to the socket and blocks if the peer has stopped reading. That’s honest back pressure rather than a buffer that grows until the machine runs out, but it means one unresponsive client can hold up a loop that broadcasts to every other one. A server with many clients and large messages should broadcast from a small pool rather than from the receiving thread.

33.3.3. Subprotocols

An endpoint that speaks more than one protocol lists them best first:

public static final class Graph implements WebSocket {
    public String[] getSubprotocols() {
        // The server's order decides, not the client's.
        return new String[]{"graphql-transport-ws", "graphql-ws"};
    }

    public void onOpen(WebSocketSession session) {
        if("graphql-ws".equals(session.getSubprotocol())) {
            // The legacy protocol; answer in its shape.
        }
    }

    public void onText(WebSocketSession session, String message) {
    }

    public void onBinary(WebSocketSession session, byte[] message, int offset, int length) {
    }
}

The server’s order decides, not the client’s, so a client can’t select a deprecated protocol over a current one by listing it first. When nothing matches, the handshake still succeeds and names no protocol, which is what the standard describes; getSubprotocol then answers null.

33.3.4. A connection, not a request

A WebSocket is a connection that stopped being HTTP, and the difference is something an author can feel.

Under virtual threads — the default on the packaged runtime — each connection owns one, and a parked virtual thread costs a stack rather than an operating system thread. Ten thousand mostly silent connections is the workload that model was built for.

On the thread pool it’s a worker per connection for as long as the connection lasts. That’s the mode the local development loop always runs in, because the JVM has no virtual threads here, and it’s also the mode any TLS server runs in. A pooled deployment serving many WebSockets has to size workers for them, because unlike a request they don’t give the worker back.

That makes workers the ceiling on concurrent connections in pool mode, and going past it fails in a way worth knowing about: the process stays healthy, the listener stays bound, and new connections are simply refused, because no worker ever comes back to accept them. An eight-worker server driven by the conformance suite reached case 9.4.4 and refused everything after it. The server says so now — it logs once when WebSockets hold half the pool — and the two ways out are more workers or a shorter CN1_WS_IDLE_TIMEOUT_MS, so abandoned connections give their worker back sooner. On virtual threads none of this applies.

The idle timeout is separate for the same reason. CN1_HTTP_TIMEOUT_MS sheds a client that began a request and stopped; a WebSocket is idle by design and would be shed within seconds by that rule. CN1_WS_IDLE_TIMEOUT_MS governs these instead, defaults to five minutes, and accepts 0 for connections that may stay silent indefinitely. CN1_WS_MAX_MESSAGE_MB bounds reassembly, because a message is as large as the peer chooses to make it.

getMetrics() reports webSocketConnections, and an open session doesn’t count towards activeRequests — it’s a connection, and counting it would make an idle server read as permanently saturated.

stop() sends every open connection a 1001 "going away" close before the drain window, so a shutdown reads as one to the client rather than as a network failure.

33.3.5. How the frame layer is checked

Most of RFC 6455 is about frames a conformant client never sends, which is how a server ends up wrong in ways nothing it talks to will reveal. The Autobahn|Testsuite is what finds those, and vm/backend/ws-conformance.sh runs it:

vm/backend/ws-conformance.sh --arm javase     # or --arm native

It needs Docker or Podman, starts an echo server, and drives ~300 cases through it. The gate has no per-case tolerance: every case must pass, and NON-STRICT counts as a failure, because a lenient frame parser is the thing this is looking for. The set of cases that ran also has to match a committed manifest, so the suite can’t shrink unnoticed to the ones that happen to pass.

Sections 12 and 13 are excluded while permessage-deflate is unimplemented. They’re excluded rather than tolerated: accepting their result as a pass would also accept it for a case that used to work.

34. Backend beans and dependency injection

A server that does everything in its controllers turns into classes nobody can test. This chapter covers the alternative Spring made standard: split the work into beans, let each one declare what it depends on, and have something else build them and hand them over. Here that something is the build, which is the difference worth keeping in mind throughout: every decision below is made while the project compiles, and a decision the build can’t make is an error before the server ever starts.

34.1. Services and dependency injection

A controller that does everything itself turns into a class nobody can test. The usual split is Spring’s: controllers translate HTTP, services hold the logic, repositories hold the SQL, and each one receives what it depends on instead of building it. The annotations are Spring’s too, under com.codename1.backend.annotations:

@Component
public class Signups {
    private final DataSource db;

    @Autowired
    private Mailer mailer;

    public Signups(DataSource db) {
        this.db = db;
    }

    @PostConstruct
    void createTable() throws IOException {
        db.execute("CREATE TABLE IF NOT EXISTS signup (email VARCHAR(200))", null);
    }
@RestController
public class SignupApi {
    private final Signups signups;

    public SignupApi(Signups signups) {
        this.signups = signups;
    }

    @PostMapping("/signups")
    public String signUp(@RequestParam("email") String email) throws Exception {
        signups.register(email);
        return "ok";
    }

    @GetMapping("/signups/count")
    public String count() throws Exception {
        return String.valueOf(signups.count());
    }
}

@Component marks a bean; a @RestController, @WebSocketMapping or @Configuration class is one too. A class with one constructor gets that constructor; with several, the one marked @Autowired. Fields and setters are injected only when marked @Autowired, and a private field is fine.

What makes this different from Spring is when it happens. The build finds every bean, decides which constructor each gets and which bean fills each injection point, and writes that down as a class called BackendWiring — a list of new calls, setter calls and @PostConstruct calls in dependency order. At run time there is no container, no classpath scan and no reflection, so a server with twenty beans starts as fast as one with none, and the translator can still drop every class nothing reaches. A dependency that can’t be satisfied is a build error that names the injection point, rather than a server that fails its first request:

constructor parameter 1 of com.example.SignupApi needs a com.example.Signups,
and no bean has that type. Annotate the implementing class @Component,
or declare a @Bean method returning one.

Two beans of one type are an error too, until one is marked @Primary or the injection point names one with @Qualifier. Constructors that depend on each other in a circle are refused; the same circle through @Autowired fields is fine, because fields are set after every bean exists.

34.2. Declaring beans

A bean is a class the build constructs once per server and hands to everything that needs it. These annotations make a class one:

AnnotationUse it for

@Component

Anything the application wires together.

@Configuration

A class whose @Bean methods produce beans, described under Section 34.5, “Profiles and factory methods”. It’s a bean itself, so it’s injected too.

@RestController, @WebSocketMapping

A controller or a WebSocket endpoint. Both are beans, and both are always singletons.

A bean’s name is the simple class name with its first letter lowercased — smtpMailer for SmtpMailer — unless the annotation gives one, as @Component("invoice") does. Names matter only to @Qualifier, and two beans with the same name are a build error.

A bean has to be a class the build can construct: concrete, and static if it’s nested. It also has to be public unless it’s in the package the generated entry point lives in, since the generated code has to name it.

34.3. Injection points

A bean receives its dependencies in three places, all resolved by type:

  • The constructor. A class with one constructor gets that one. With several, the build takes the one marked @Autowired, or failing that the one with no arguments; with neither it can’t choose and says so. Constructor injection is the one to prefer, because the object is complete the moment it exists and a test builds it with new.

  • A field marked @Autowired. A private field is fine: the build adds a setter to the compiled class and the generated code calls it, so there is no reflection. A final field can only be set by the constructor, and marking one @Autowired is a build error that says so.

  • A method marked @Autowired, called once with its parameters resolved like a constructor’s.

Injection points and lifecycle methods declared in a superclass count too, and are handled before the subclass’s own, and the same goes for @Scheduled, @McpTool and managed methods: a job a base class declares runs for every bean that inherits it. A method the subclass overrides follows the subclass’s annotations, not the superclass’s. The build can only do this for a superclass compiled from the project’s sources: a base class from a jar that has injected fields, or lifecycle methods that aren’t public, is a build error telling you to take those dependencies in the subclass instead.

@Autowired(required = false) leaves the point null when no bean has its type, where a required point is a build error. A List<T> or Collection<T> receives every bean of type T:

@Component
public class Checkout {
    private final PaymentProvider preferred;       // the @Primary one: CardPayments

    @Autowired
    @Qualifier("invoice")
    private PaymentProvider invoice;               // the one named "invoice"

    @Autowired
    private List<PaymentProvider> all;             // every PaymentProvider bean

    @Autowired(required = false)
    private Audit audit;                           // null when no bean provides one

    public Checkout(PaymentProvider preferred) {
        this.preferred = preferred;
    }

When several beans have the injected type and the point names none, the build takes the one marked @Primary:

@Component
@Primary
public class CardPayments implements PaymentProvider {
    public String name() {
        return "card";
    }
}

Without one it’s an error listing the candidates, and two @Primary beans of one type are an error too. @Qualifier("invoice") names the bean a point wants and overrides @Primary. The single exception is a set of candidates that are all conditional — a @Profile("dev") bean and a @Profile("!dev") one — where at most one exists at run time and the generated code picks whichever does.

Some types aren’t beans the application declares, but the server provides them and they’re injected the same way:

TypeWhat it receives

Config

The server’s configuration, for keys read at run time.

DataSource

The connection pool. A bean that takes one makes a database required, and a server with none configured refuses to start with a message naming the bean.

orm.EntityManager

The entity manager over that pool, when the module has entities.

com.codename1.orm.session.Session

The current transaction’s persistence session, described under Chapter 36, Backend data access and transactions.

HttpServer.Request

The current request. Only a @Scope("request") bean can take it.

HttpSession

The current session. Only a @Scope("request") bean can take it.

34.4. Configuration values

@Value reads a setting, with a fallback after the colon, and converts it to the field’s type:

@Component
@Profile("!dev")
public class SmtpMailer implements Mailer {
    @Value("${mail.host:localhost}")
    private String host;

    @Value("${mail.port:25}")
    private int port;

    public void send(String to, String subject, String body) {
        // ... talk to host:port ...
    }
}

The value comes from the layers described under Section 32.8, “Configuration and profiles”, so MAIL_HOST in the environment overrides the file. A key with no fallback that nothing sets stops the server at startup with the key’s name. @ConfigurationProperties("mail") on a bean does the same for every one of its setters at once: setHost reads mail.host, and setMaxSize reads mail.maxSize or mail.max-size.

@Value converts to a String, a primitive or its box, or an enum; a field of any other type is a build error. A key that has no fallback and isn’t set in application.properties gets a build warning, since it can still come from the environment at run time.

@ConfigurationProperties suits a group of related settings better than a @Value per field, and a bean that holds them is an ordinary bean that others inject:

@Component
@ConfigurationProperties("mail")
public class MailSettings {
    private String host = "localhost";   // kept when mail.host is not set
    private int port = 25;
    private boolean startTls;

    public void setHost(String host) {          // mail.host
        this.host = host;
    }

    public void setPort(int port) {             // mail.port
        this.port = port;
    }

    public void setStartTls(boolean startTls) { // mail.startTls or mail.start-tls
        this.startTls = startTls;
    }

Each setter taking a String, a number, a boolean or an enum is bound from the prefix plus the property name, in camel case or kebab case. A setter whose key isn’t set is never called, so the field keeps its initial value — which is where a default belongs. A prefix that binds no setter at all is a build error, since it means the class and the prefix disagree.

34.5. Profiles and factory methods

@Profile and @ConditionalOnProperty make a bean depend on the deployment:

@Component
@Profile("dev")
public class ConsoleMailer implements Mailer {
    public void send(String to, String subject, String body) {
        System.out.println("mail to " + to + ": " + subject);
    }
}

The profile is the one choice the build can’t make for you, so it becomes a single if in the generated startup code. @ConditionalOnMissingBean is decided entirely at build time: the bean steps aside when the application declares another of its type.

A class from a library can’t carry annotations. A @Bean method in a @Configuration class makes one:

@Configuration
public class Limits {
    @Bean
    public RateLimiter signupLimiter(@Value("${signups.perMinute:30}") int perMinute) {
        return new RateLimiter(perMinute);
    }
}

The build calls the method once, directly. There is no proxy around the configuration class, so one @Bean method calling another gets a second object; take the other bean as a parameter instead, which the build fills like a constructor’s.

34.5.1. Conditions in detail

@Profile takes one or more profile names, and the bean exists when any of them is active. A name starting with ! is negated, so @Profile("!prod") covers every profile but prod.

@ConditionalOnProperty makes a bean depend on a key:

@Component
@ConditionalOnProperty(value = "search.enabled", havingValue = "true")
public class SearchIndexer {
    public void index(String document) {
        // ...
    }
}

With havingValue, the key must equal that value, compared ignoring case. Without it, any value but false counts. matchIfMissing = true makes an unset key count as a match, and several keys in value must all match. prefix is put in front of each key with a dot.

Both are evaluated once, at start-up, as one if around the bean’s construction. Which beans are on depends on the deployment’s configuration, so it’s the start that checks it, as Spring’s does: a bean that requires one whose conditions are all off under the running configuration stops the server from starting, with an error naming the injection point. Cover the other case with a bean of the same type under the opposite condition, make the dependency optional, or give the dependent bean the same condition.

@ConditionalOnMissingBean is the one condition decided entirely at build time. It marks a default that steps aside when the application declares another bean it would compete with, which is how a module offers a fallback:

@Configuration
public class Defaults {
    @Bean
    @ConditionalOnMissingBean
    public Audit audit() {
        return new LogAudit();      // used only when the application declares no Audit
    }
}

With no arguments, "another bean it would compete with" means one that has any of the types the default can be injected as, apart from the JDK’s own: its class, its superclasses and its interfaces. On a class, DefaultMailer implements Mailer steps aside for any other Mailer. On a @Bean method they’re the declared return type and the types above it, which is why the method above returns Audit rather than LogAudit. @ConditionalOnMissingBean(Audit.class) names the types to compete on explicitly, for a class whose default set is too wide or too narrow.

A @Bean method inherits the conditions of the @Configuration class it’s declared in, static methods included. A @Profile("prod") configuration class therefore contributes nothing on any other profile, which is what the annotation on the class says. The same holds for @ConditionalOnMissingBean on the class: a configuration class that steps aside takes its @Bean methods with it.

34.5.2. Beans with a lifecycle of their own

A library class often has methods to start and stop it, and no annotations to say so. @Bean names them:

@Configuration
public class SearchConfig {
    @Bean(initMethod = "connect", destroyMethod = "close")
    public SearchClient searchClient(@Value("${search.url:http://localhost:9200}") String url) {
        return new SearchClient(url);
    }
}

initMethod runs after the factory method returns, where a class’s @PostConstruct would. destroyMethod runs where its @PreDestroy would: at shutdown for a singleton, at the end of the request for a request-scoped bean, and when the session ends for a session-scoped one.

34.6. Scopes

A bean is a singleton unless it says otherwise. @Scope("prototype") gives each injection point its own instance. @Scope("request") and @Scope("session") give one per HTTP request or session; injected into a singleton, such a bean is reached through a small generated subclass that finds the current request’s instance on each call, so the class can’t be final and needs a constructor that takes no arguments. @Lazy builds a bean the first time it’s used, through the same kind of subclass. That subclass has to run the bean’s constructor once at startup to exist at all, so the expensive part of setting a bean up belongs in its @PostConstruct method, which runs only on the real instance.

ScopeInstances

singleton, the default

One per server, built at start-up in dependency order.

@Scope("prototype")

A new instance for each injection point. Two fields of type Ticket in one class get two tickets.

@Scope("request")

One per HTTP request, built the first time the request uses it and destroyed when the request ends.

@Scope("session")

One per HTTP session, kept by the server for as long as the session lives and destroyed when it ends. See Chapter 37, Backend sessions.

@Lazy, on a singleton

One per server, built the first time something calls it.

A request-scoped bean looks like any other to the code that uses it:

@Component
@Scope("request")
public class RequestClock {
    private final long started = System.currentTimeMillis();

    public long elapsed() {
        return System.currentTimeMillis() - started;   // since THIS request's instance
    }
}

A controller that injects RequestClock gets the generated stand-in, and each call on it reaches the instance belonging to the request being served. Two requests served at once see two instances.

The stand-in extends the bean’s class and overrides its methods, which is where the rules for a scoped or lazy bean come from. The class can’t be final, a method it declares or inherits can’t be final either — a call to it would run on the stand-in instead of the bean — and it needs a constructor with no arguments, which may be protected. Methods inherited from a library class are forwarded too, so the library has to be on the build’s classpath. Inject anything else through fields. Each rule is checked by the build.

@Lazy suits a bean that’s expensive to prepare and not always needed:

@Component
@Lazy
public class ExpensiveIndex {
    private Map<String, String> index;

    public ExpensiveIndex() {
        // Runs at start-up for the generated stand-in too: keep it cheap.
    }

    @PostConstruct
    void load() {
        // Runs once, on the real instance, the first time something calls it.
        index = new HashMap<String, String>();
    }

    public String lookup(String key) {
        return index.get(key);
    }
}

A WebSocket endpoint must be a singleton, since it serves the whole life of the server rather than one request. A bean with @Scheduled methods can’t be request- or session-scoped, because a job runs outside any request. A prototype-scoped one only draws a warning, as in Spring: one instance is built to run its jobs and is never destroyed, so make it a singleton if it needs @PreDestroy.

34.7. Lifecycle

Beans are built in dependency order, and a bean’s @PostConstruct method runs once it and everything it depends on has been built and injected. At shutdown, @PreDestroy methods run in the reverse order, so a bean’s dependencies are still there when its own method runs. Within one bean the same rule holds for its class hierarchy: a superclass’s @PostConstruct runs before the subclass’s, and a subclass’s @PreDestroy runs before the superclass’s:

@Component
public class Outbox {
    private final DataSource db;
    private final List<String> pending = new ArrayList<String>();

    public Outbox(DataSource db) {
        this.db = db;
    }

    @PostConstruct
    void createTable() throws IOException {
        // Every dependency is built, injected and initialized by now.
        db.execute("CREATE TABLE IF NOT EXISTS outbox (message VARCHAR(500))", null);
    }

    public synchronized void add(String message) {
        pending.add(message);
    }

    @PreDestroy
    synchronized void flush() throws IOException {
        // After the last request has finished, before the pool closes.
        for (String message : pending) {
            db.execute("INSERT INTO outbox (message) VALUES (?)", new Object[] {message});
        }
        pending.clear();
    }
}

Both methods take no arguments and may have any visibility; the build adds a public bridge to reach a private one. Stopping a server goes in this order, which is what makes the flush above safe:

  1. Scheduled jobs stop starting, so none begins while the server drains.

  2. The server stops accepting and waits, up to cn1.server.shutdownTimeoutMillis, for the requests in flight.

  3. @Async calls and scheduled runs that haven’t finished get the same grace. A task still waiting in a queue when that time is up is dropped rather than started against beans about to be destroyed, and the threads of the ones still running are interrupted.

  4. @PreDestroy and destroyMethod run, in reverse dependency order.

  5. The connection pool closes.

  6. The last metrics and spans are exported.

A start-up that fails partway runs the destroy methods of the beans already built, so a server a supervisor restarts doesn’t leak what their constructors opened.

34.8. What the build refuses

Everything a container would discover at start-up is checked while the module compiles, and each refusal names the class and member at fault:

Message fragmentWhat to do

needs a X, and no bean has that type

Annotate the implementation @Component, or add a @Bean method returning one.

could receive any of …​

Mark one @Primary, or inject with @Qualifier("name").

The constructors form a cycle

Inject one side through an @Autowired field or setter, which is set after every bean exists.

is final, so it can only be set by the constructor

Take the dependency as a constructor parameter.

and none is marked @Autowired or takes no arguments

Mark the constructor the build should call.

is reached through a class the build generates to stand in for it

A scoped or lazy bean: make the class and its methods non-final and give it a constructor with no arguments.

asks for the current request

Only a @Scope("request") bean can inject the request. Take it as a parameter of the handler method instead.

Two beans are named

Give one a name of its own in its annotation.

34.9. How this differs from Spring

The annotations mean what they mean in Spring, and most code reads the same. The differences all follow from doing the work at build time:

  • No scanning. A bean is a class in the module, or the result of a @Bean method. A jar on the class path contributes nothing unless a @Bean method builds something from it.

  • No proxies for aspects. @Transactional, @Async, @Timed and @Counted rewrite the method itself, so they apply to a call through this, to a private method, and to an object built with new. In Spring each of those skips the aspect, and nothing says so.

  • No proxy around @Configuration. One @Bean method calling another gets a second object. Take the other bean as a parameter instead.

  • Errors at build time. A missing or ambiguous dependency, a constructor cycle, a scoped bean that can’t be subclassed and a malformed cron expression all fail the build.

  • No ApplicationContext. Nothing looks a bean up by name or type at run time. A class that needs a bean declares it as a dependency.

34.10. Testing beans and the wired server

Constructor injection is a constructor, so a unit test builds the object with fakes and needs nothing else. For the wired server — what @SpringBootTest does in Spring — annotate a test class with @BackendTest. It starts the application with the build’s wiring, lets the test replace beans, and sends requests through MockMvc or over a real port. Chapter 35, Backend testing covers it.

35. Backend testing

A backend module’s tests are JUnit 5 classes in src/test/java, written against the API a Spring Boot test uses. @BackendTest starts the module’s server, wired the way it ships, and injects what the test asks for. MockMvc sends a request to that server inside the test process, and TestRestTemplate sends one over a real port. A test can add beans of its own and replace the application’s. This chapter covers each of those, and then the same tests compiled into a native binary.

Because the names follow Spring’s, a test ported from Spring Boot mostly needs its imports changed. The differences are listed at the end of the chapter.

35.1. Setting up a module for tests

A backend module created from the project template is already set up. In another module, add the test library and JUnit 5 to the pom.xml:

<dependency>
    <groupId>com.codenameone</groupId>
    <artifactId>codenameone-backend-test</artifactId>
    <version>${cn1.version}</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.9.3</version>
    <scope>test</scope>
</dependency>

Then bind two more goals of the Codename One plugin beside process-annotations, and give each test class a fresh JVM:

<execution>
    <id>cn1-process-test-annotations</id>
    <phase>process-test-classes</phase>
    <goals>
        <goal>process-test-annotations</goal>
    </goals>
</execution>
<execution>
    <id>cn1-compiled-tests</id>
    <phase>test</phase>
    <goals>
        <goal>backend-test</goal>
    </goals>
</execution>
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.0.0-M5</version>
    <configuration>
        <reuseForks>false</reuseForks>
    </configuration>
</plugin>

process-test-annotations generates the code each test class runs against. backend-test runs the compiled tests, and does nothing unless they’re asked for. A server process runs one backend at a time, so reuseForks set to false keeps the state of one test class’s server away from the next class.

Run the tests with:

mvn -pl backend -Dcodename1.platform=backend test

A Gradle backend gets the same setup from the Codename One plugin: the test library, JUnit 5, a fresh JVM per test class and the test pass all come with it. The standard test task runs the tests, and backendTest runs them as a native binary, as described below:

./gradlew test          # on this JVM
./gradlew backendTest   # as a native binary

35.2. Testing a controller with MockMvc

This controller answers a greeting as JSON, using a Greetings bean:

@RestController
public class GreetingApi {
    private final Greetings greetings;

    public GreetingApi(Greetings greetings) {
        this.greetings = greetings;
    }

    /** A JSON object holding the greeting. */
    @GetMapping("/greet/{name}")
    public Map<String, Object> greet(@PathVariable("name") String name) {
        Map<String, Object> out = new LinkedHashMap<>();
        out.put("greeting", greetings.greet(name));
        return out;
    }
}

A test of it reads the way a Spring MVC test does:

import com.codename1.backend.annotations.Autowired;
import com.codename1.backend.test.BackendTest;
import com.codename1.backend.test.MediaType;
import com.codename1.backend.test.MockMvc;
import org.junit.jupiter.api.Test;

import static com.codename1.backend.test.MockMvcRequestBuilders.get;
import static com.codename1.backend.test.MockMvcResultMatchers.content;
import static com.codename1.backend.test.MockMvcResultMatchers.jsonPath;
import static com.codename1.backend.test.MockMvcResultMatchers.status;

@BackendTest
class GreetingApiTest {
    @Autowired
    private MockMvc mvc;

    @Test
    void greetsByName() throws Exception {
        mvc.perform(get("/greet/{name}", "Ada"))
                .andExpect(status().isOk())
                .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$.greeting").value("Hello, Ada"));
    }

    @Test
    void unknownRoutesAreNotFound() throws Exception {
        mvc.perform(get("/nowhere")).andExpect(status().isNotFound());
    }
}

@BackendTest starts the application with the same beans and the same wiring as the server the build produces. @Autowired fields of the test class are filled from its beans, and MockMvc is one of them. perform runs a request through the server’s dispatch — the router, the sessions, the scoped beans and the error handling — without opening a socket. Each andExpect checks one thing about the response and fails the test with a message saying what it saw.

The request builders are static methods of MockMvcRequestBuilders:

MethodBuilds

get, post, put, patch, delete, head, options

A request with that method. A URI template’s {name} placeholders are filled, encoded, from the arguments that follow it.

request(HttpMethod, uri, …​)

A request with any method.

multipart(uri, …​)

A multipart/form-data POST. .file(…​) adds a file part and .param(…​) adds a form field.

Each builder takes .header, .param (a query parameter), .content (the body, as a string or bytes), .contentType, .accept, .characterEncoding and .cookie.

The matchers are static methods of MockMvcResultMatchers:

MatcherChecks

status()

The status: isOk(), isCreated(), isNotFound() and the other names, a class with is4xxClientError() and similar, or a number with is(int).

content()

The body with string, bytes or json (a JSON body holding every field of the expected one), and the content type with contentType or contentTypeCompatibleWith.

jsonPath(expression)

A value in a JSON body: value, exists, doesNotExist, isArray, isEmpty and the type checks. The expression supports $, .name, ['name'], [index] (negative counts from the end), [], . and a final .length().

header()

A response header: string, longValue, exists or doesNotExist.

cookie()

A cookie the response sets: value, exists or doesNotExist.

redirectedUrl(url)

The Location of a redirect.

andExpectAll checks several matchers and reports every one that failed rather than stopping at the first. andDo(print()) writes the request and response to standard output. andReturn() gives the result, for assertions no matcher covers:

String body = mvc.perform(get("/greet/{name}", "Ada"))
        .andReturn().getResponse().getContentAsString();

35.3. Testing over a real port

MockMvc skips the socket, the HTTP parser and the response writer. It also skips the checks the parser makes on a request target, so a malformed one such as /users/%FF reaches the routes instead of getting a 400. To test through them, serve the application on a free port:

import com.codename1.backend.annotations.Autowired;
import com.codename1.backend.test.BackendTest;
import com.codename1.backend.test.LocalServerPort;
import com.codename1.backend.test.ResponseEntity;
import com.codename1.backend.test.TestRestTemplate;
import org.junit.jupiter.api.Test;

import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

@BackendTest(webEnvironment = BackendTest.WebEnvironment.RANDOM_PORT)
class ServedGreetingTest {
    @Autowired
    private TestRestTemplate rest;

    @LocalServerPort
    private int port;

    @Test
    void answersOverARealSocket() throws Exception {
        assertTrue(port > 0);
        ResponseEntity<Map> answer = rest.getForEntity("/greet/{name}", Map.class, "Ada");
        assertEquals(200, answer.getStatusCodeValue());
        assertEquals("Hello, Ada", answer.getBody().get("greeting"));
    }
}

With RANDOM_PORT, the test injects TestRestTemplate, an HTTP client for the running server, and @LocalServerPort holds the port it chose. The client resolves paths against the server, so a test never builds a URL. It encodes a body as JSON and decodes an answer into a String, a byte[], a Map or a List. getForObject returns the body, and getForEntity returns a ResponseEntity with the status and headers too. postForEntity, put, delete and exchange cover the other methods, and withBasicAuth returns a client that signs every request in. A status of 4xx or 5xx is an answer, not an exception, so a test can assert on it.

The client is the backend’s own, so a compiled test run sends its requests from the native binary’s HTTP stack.

webEnvironmentThe application is

MOCK (the default)

Reached through MockMvc, in the test’s process.

RANDOM_PORT

Served on a free port of the loopback interface.

DEFINED_PORT

Served on the configured cn1.server.port.

NONE

Started for its beans only; the test calls them directly.

Every environment but DEFINED_PORT listens on a free loopback port, so test runs never collide with each other or with a server running on the machine.

35.4. Settings and profiles

A test runs with the module’s application.properties, then the profile’s file, then the settings the annotation names:

@BackendTest(properties = {"app.greeting=Hi", "cn1.session.timeout=5"})

The settings a test names win over everything, the environment included, unlike a deployed server, where the environment has the last word. A shell that exports PORT or DATABASE_URL therefore doesn’t move a test off its free port or its test database.

The profile is test unless profile names another, and test is a development profile: unless the test or the profile’s file names a database, the application gets an in-memory SQLite one, and the generated daos create their tables. A test therefore starts from an empty database with no setup.

Starting an application costs time, so test classes share one. Classes with the same test beans, settings, profile and web environment use the same running application, much as Spring caches a context. A class whose configuration differs stops it and starts its own. State that a test leaves in a bean or the database is visible to the next test that shares the application, so a test must not depend on the order tests run in.

35.5. Replacing a bean

A static nested class marked @TestConfiguration adds its @Bean methods to the test’s application. Marked @Primary, a test bean wins every injection of its type, which replaces the application’s bean without a mocking library:

@TestConfiguration
static class Fakes {
    @Bean
    @Primary
    public Greetings formalGreetings() {
        return new Greetings() {
            @Override
            public String greet(String name) {
                return "Good evening, " + name;
            }
        };
    }
}

@Test
void theControllerGetsTheTestBean() throws Exception {
    mvc.perform(get("/greet/{name}", "Ada"))
            .andExpect(jsonPath("$.greeting").value("Good evening, Ada"));
}

A top-level @TestConfiguration class is added by listing it in @BackendTest(classes = …​). A test bean is an ordinary singleton: it receives its own injections, and the test can @Autowired it. It can’t be request-scoped, session-scoped or lazy, and it can’t carry @Transactional, @Async or the other annotations the build weaves; the build says so when one does.

35.6. Mocking a bean with Mockito

With org.mockito:mockito-core on the test classpath, @MockitoBean replaces every bean of the field’s type with a Mockito mock and injects it:

@MockitoBean
private Greetings greetings;

@Test
void theControllerCallsTheMock() throws Exception {
    when(greetings.greet("Ada")).thenReturn("Hi, Ada");
    mvc.perform(get("/greet/{name}", "Ada"))
            .andExpect(jsonPath("$.greeting").value("Hi, Ada"));
    verify(greetings).greet("Ada");
}

The mock is reset after every test. A controller can’t be mocked, because it’s what the test is exercising. Mockito builds classes while it runs, which a native binary can’t do, so @MockitoBean tests run only on the JVM. A @Primary test bean works in both.

35.7. Running the tests compiled

The server ships as a native binary, and the tests can run that way too:

mvn -pl backend -Dcodename1.platform=backend test -Dcn1.backend.compiledTests=true

After Surefire runs the tests on the JVM, the backend-test goal compiles the same test classes with the application into one native binary, the way cn1:backend-package builds the server, then runs it. The test binary has the generated wiring, the woven methods and the natives of the shipped server, so a difference between the two runtimes fails a test rather than a deployment.

The binary has no JUnit and no reflection. The build finds the tests in the compiled classes and generates the calls JUnit would make: @BeforeAll, @BeforeEach, @Test, @AfterEach and @AfterAll, with @Disabled and assumptions honored. The tests compile against a subset of JUnit 5 that the test library provides for the purpose. It covers those annotations, @DisplayName (reports keep the method name, as Surefire’s do by default) and @Tag, Assumptions, and the Assertions methods: assertEquals, assertNotEquals, assertTrue, assertFalse, assertNull, assertNotNull, assertSame, assertNotSame, assertArrayEquals, assertIterableEquals, assertInstanceOf, assertThrows, assertThrowsExactly, assertDoesNotThrow, assertAll and fail.

The compiled run picks the same test classes as the JVM run: under Maven, the ones Surefire’s includes and excludes select (its defaults, unless the pom names others), or the ones -Dtest names, down to the methods a Class#method entry selects; under Gradle, every test class. Tag selection isn’t applied to it.

@BackendTest is inherited, so an abstract base class can carry it with the shared fields and tests. Each concrete subclass gets its own context, and tests declared as default methods of an interface the class implements run too.

The results go to target/surefire-reports as TEST-<class>-compiled.xml, beside the JVM run’s reports, and a failed test fails the build.

A compiled run has these limits:

  • Mockito. A test class whose code uses Mockito is left out of the binary, and a class with a @MockitoBean field is reported as skipped. Pass -Dcn1.backend.compiledTests.strict=true to make either one fail the build.

  • JUnit and Surefire subsets. The compiled run reads the common JUnit 5 annotations and Surefire selections, not every one. Repeating @Tag on a class or method stops the compiled run from compiling, so use one per element. A -Dtest regex selector with a method part, such as %regex[.Api.#one.*], selects nothing in the compiled run. The JVM run handles both. The compiled run reads the inline <includes> and <excludes> lists one pattern per entry: it ignores includesFile and excludesFile, and doesn’t split comma-separated or !-negated patterns inside one entry.

  • Nested classes. The compiled run doesn’t run the tests of a @Nested class. A @Nested class inside a @BackendTest class isn’t supported on either run; give it a top-level @BackendTest class of its own.

  • Java tests only. The binary is compiled from Java test sources. A module with Kotlin tests runs them on the JVM, and the compiled run reports them by name and stops rather than running without them. Gradle builds look for Kotlin tests in src/test/kotlin only.

  • Toolchain. The binary needs what cn1:backend-package needs: clang, and the OpenSSL, libcurl and nghttp2 headers. It builds on Linux and macOS. On Windows, which has no native backend, the goal reports that and does nothing.

  • Time. Building the binary takes about as long as packaging the server, so it suits CI better than an edit-and-run loop.

35.8. Differences from Spring Boot

  • Packages. The annotations and classes live in com.codename1.backend.test, and @Autowired is com.codename1.backend.annotations.Autowired.

  • @BackendTest instead of @SpringBootTest. Its options are the web environment, properties, profile and classes. @WebMvcTest-style slices don’t exist; every test runs the whole application.

  • Wiring at build time. The test’s application is generated by the build rather than assembled when the test starts. An injection that can’t be satisfied fails the build.

  • One application at a time. Spring can keep several contexts cached. Here a class with a different configuration stops the running application before it starts its own.

  • A listener in every environment. MOCK and NONE also start the application listening on a free loopback port, because its server hosts the threads its tasks run on.

  • The JSON path subset. jsonPath supports the expressions listed above, without filters or functions other than length().

36. Backend data access and transactions

A server’s data lives in a database, and this chapter is about reaching it: the connection pool, which speaks to SQLite, PostgreSQL and MySQL with one dialect of SQL, the object mapping the build generates daos from, and the transactions that make several statements one unit of work. The last of those gets the most room, because declarative transactions are where a Spring developer’s intuitions are most likely to be almost right.

36.1. Talking to a database

The server opens a connection pool from cn1.datasource.url — a SQLite path or a PostgreSQL or MySQL URL — and injects it as a DataSource into any bean that asks for one. It returns rows as the same Java types whichever engine answered:

// db is the pool the server opened from cn1.datasource.url and injected
List rows = db.query("SELECT id, body FROM note WHERE id > ?",
                     new Object[] { Integer.valueOf(10) });

There is no JDBC driver involved. SQLite is linked into the binary, and the PostgreSQL and MySQL clients speak their wire protocols directly, and a mariadb:// URL is served by the same client. MySQL 8.0 and MariaDB 10.4 and newer are supported: a string key needs a case-sensitive NO PAD collation, so "A" and "a" are two keys and "token " keeps its space, and the two server families spell that collation differently. Which one is used comes from the server rather than from the URL scheme, so a mysql:// URL pointed at MariaDB is still correct.

Statements are written once, in one portable form: ? for every parameter, and plain unquoted names. PostgreSQL binds $1 rather than ?, and that difference stops inside execute and query rather than at every call site. SQL already written for one engine keeps working, because a statement carrying no ? at all is passed through untouched — so hand-written $1 is left alone. A literal question mark that isn’t a parameter is written ??, which matters on PostgreSQL, whose jsonb operators are spelled ?, ?| and ?&.

A parameter count that doesn’t match the statement is refused before the statement is sent. The three engines answer a mismatch three different ways, and SQLite’s answer is to bind the missing parameters to NULL and commit the row.

The other thing the engines disagree about is the key an insert generated. insert asks whichever way this engine answers — last_insert_rowid() on SQLite, LAST_INSERT_ID() on MySQL, and INSERT …​ RETURNING on PostgreSQL, which has no last-insert-id concept at all:

long id = db.insert("INSERT INTO note (body) VALUES (?)",
                    new Object[] { "first" }, "id");

Several statements that have to be one go through inTransaction, which holds a single connection for the whole body and rolls back if it throws:

db.inTransaction(new DataSource.Work() {
    public Object run(Database connection) throws Exception {
        connection.execute("UPDATE account SET balance = balance - ? WHERE id = ?",
                           new Object[] { Integer.valueOf(100), Integer.valueOf(1) });
        connection.execute("UPDATE account SET balance = balance + ? WHERE id = ?",
                           new Object[] { Integer.valueOf(100), Integer.valueOf(2) });
        return null;
    }
});

Everything each engine spells differently is reachable through db.dialect(), which is what code that generates schema needs:

Dialect dialect = db.dialect();
String create = "CREATE TABLE IF NOT EXISTS " + dialect.quote("note") + " ("
        + dialect.quote("id") + " " + dialect.generatedKeyColumn(Dialect.BIGINT) + ", "
        + dialect.quote("body") + " " + dialect.columnType(Dialect.TEXT) + ")";

Quoting every identifier isn’t caution. PostgreSQL folds an unquoted name to lower case while SQLite and MySQL preserve it, so a column called createdAt becomes createdat on one engine of the three, and code that reads rows by name stops finding it there.

A Database — one connection rather than a pool — is still available through Database.open for code that wants exactly one, and every one of its operations is synchronized, so sharing one across handlers is safe and serialized. That’s the right shape for a single-file SQLite server and the wrong one for a database that’s a machine across a network: there the single connection isn’t a safety property, it’s the bottleneck. The pool is the default for that reason.

36.2. Storing objects

A class with @Entity on it gets a data access object written for it at build time. The annotations are the ones the SQLite ORM chapter documents, and they are the same annotations in the same package, because an entity is the one class both halves of an application own:

@Entity(table = "reminders")
public class Reminder {
    @Id public long id;

    @Column(nullable = false) public String title;

    public java.util.Date due;

    public boolean done;

    @DbTransient public String cachedLabel;   // never stored

    public Reminder() {
    }
}

What differs between the app’s copy and the server’s is what the build generates from it: a module compiled against the Codename One core gets a dao over the local SQLite database, and a module compiled against this runtime gets one whose statements are built for whichever engine the connection turns out to be. The class says what the data is; the module says where its rows live.

Dao<Reminder> reminders = em.dao(Reminder.class);

Reminder reminder = new Reminder();
reminder.title = "renew the certificate";
reminder.due = new Date();
reminders.insert(reminder);          // reminder.id is now the generated key

Reminder stored = reminders.findById(Long.valueOf(reminder.id));
stored.done = true;
reminders.update(stored);

The entity manager comes from the entry point, which opens one when the build generated at least one entity. A controller asks for it by declaring a constructor that takes one, and the generated entry point calls that constructor:

@RestController
@RequestMapping("/reminders")
public class ReminderApi {
    private final Dao<Reminder> reminders;

    /** The entry point calls this one because it is the one declared. */
    public ReminderApi(EntityManager entities) {
        this.reminders = entities.dao(Reminder.class);
    }

    @GetMapping
    public List<Reminder> outstanding() throws IOException {
        return reminders.query().eq("done", Boolean.FALSE).orderBy("due", true).list();
    }
}

A controller is a bean, so its constructor can take the entity manager, the pool, or any service built on them, as Chapter 34, Backend beans and dependency injection describes. The generated entry point holds a new with the argument written into it, and a controller that needs a database nothing configured is refused at start-up rather than handed a null to fail on later.

The build writes the entry point, and the daos are registered there: this runtime has no reflection and the translator drops a class nothing references, so the generated code’s direct reference to every dao is what keeps them in the binary. Put start-up work in a bean’s @PostConstruct method.

Outside the server — in a unit test, say — an entity manager is a single call over a pool:

EntityManager em = EntityManager.open(pool);

Queries name JAVA FIELDS rather than columns, and the builder quotes the column each one maps to:

List<Reminder> overdue = em.dao(Reminder.class).query()
        .eq("done", Boolean.FALSE)
        .lt("due", new Date())
        .orderBy("due", true)
        .limit(20)
        .list();

A name that isn’t a field of the entity is refused at once, listing the ones that are, rather than reaching the server as a column it doesn’t have. eq, ne, gt, gte, lt, lte, like, in, isNull and isNotNull are joined with AND in the order they were added, and list, first, count and delete end the chain. A query the builder can’t express takes SQL instead, through dao.find(where, params), which is the point at which portability becomes yours to keep.

Transactions take the same shape as everywhere else: the entity manager the body is handed is pinned to one connection, so every dao reached through it runs inside the transaction. Called inside a @Transactional method, it joins that transaction instead of starting another.

em.transaction(new EntityManager.Work() {
    public Object run(EntityManager tx) throws Exception {
        Dao<Reminder> reminders = tx.dao(Reminder.class);
        Reminder first = reminders.findById(Long.valueOf(1));
        first.done = true;
        reminders.update(first);
        reminders.insert(follower(first));
        return null;
    }
});

Three things this doesn’t do, on purpose. Relationships aren’t supported — @OneToMany and friends don’t exist, and a field referencing another entity fails the build with a message saying to persist the foreign key as a scalar. createTable creates a table that isn’t there and does nothing at all to one that is, so it’s a convenience for development and for tests rather than a migration tool. And a boolean, a date and a char are stored as integers on every engine — 0 or 1, epoch milliseconds, and the UTF-16 code unit — because a native timestamp comes back as text whose format follows the server’s own time zone and would not round-trip the same way on three engines, and because a char that was never assigned holds NUL, which PostgreSQL refuses inside a text value. A field declared as a primitive gets a NOT NULL column, because a primitive has no null to read: a nullable column would load as 0, false or \0, which is indistinguishable from a row that holds those. Declare the field as its boxed type — Integer rather than int — when the column can be empty.

36.3. Transactions

A transaction makes several statements succeed or fail together. @Transactional declares one around a method, and the build writes the code that begins, commits and rolls back the transaction around the method’s body:

@Transactional(rollbackFor = IOException.class)
public void register(String email) throws IOException {
    db.execute("INSERT INTO signup (email) VALUES (?)", new Object[] {email});
    // Throws when the mail server refuses: the insert above is rolled back,
    // because both run in the one transaction this method began. A failed
    // statement would roll back on its own -- it is a DataAccessException --
    // but the mailer's IOException is checked, so it takes rollbackFor.
    mailer.send(email, "Welcome", "Thanks for signing up.");
}

Everything the method does through the server’s DataSource joins the transaction without being handed anything, and so does every method it calls on the same thread. On a class, @Transactional applies to each public method the class declares. As in Spring, a method it inherits from a superclass isn’t covered until the class overrides it, and the build warns about each one. The same holds for @Async on a class.

36.3.1. How a method joins a transaction

The transaction belongs to the thread that began it. When a thread inside one asks the pool for a connection — directly, through an entity manager’s daos, or through a DataSource method — it gets the transaction’s connection back instead of a pooled one. That’s why a service three calls deep takes part without a parameter for it, and why the programmatic forms, DataSource.inTransaction and EntityManager.transaction, run as part of an open transaction rather than starting a second one.

The connection is borrowed, and BEGIN sent, only when the method first touches the database. A @Transactional method that returns early, or that turns out to have nothing to write, never takes a connection at all. The same laziness settles which database a transaction is on: the first pool it touches. Statements through any other pool run outside it, each committing on its own.

A transaction doesn’t follow work to another thread. An @Async method, a task given to Tasks, or a scheduled job runs outside the caller’s transaction, and begins its own if it’s @Transactional itself.

36.3.2. Propagation

propagation says what a method does when it’s called with a transaction already open. The default, REQUIRED, is right for most methods: join the open transaction, or begin one if there is none.

Three timelines: REQUIRED sharing one connection, REQUIRES_NEW suspending the outer transaction and committing on a second connection, NESTED using a savepoint
Figure 248. What each propagation does when a transaction is already open
PropagationWith a transaction openWith none open

REQUIRED

Joins it.

Begins one.

REQUIRES_NEW

Suspends it and begins another, on a second connection.

Begins one.

NESTED

Sets a savepoint in it.

Begins one.

SUPPORTS

Joins it.

Runs without one.

MANDATORY

Joins it.

Throws TransactionException.IllegalState.

NOT_SUPPORTED

Suspends it and runs without one.

Runs without one.

NEVER

Throws TransactionException.IllegalState.

Runs without one.

REQUIRES_NEW suits work that must be kept whatever happens to the caller, an audit record being the usual case:

@Component
public class AuditLog {
    private final DataSource db;

    public AuditLog(DataSource db) {
        this.db = db;
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void record(String event) throws IOException {
        // Commits on its own connection, whatever the caller's transaction does.
        db.execute("INSERT INTO audit (event) VALUES (?)", new Object[] {event});
    }
}

It costs a second connection for as long as it runs, taken from the same pool while the first one is still held. See the pitfalls at the end of this chapter before using it on SQLite.

36.3.3. Rollback rules

The rules are Spring’s. An unchecked exception or an Error leaving the method rolls the transaction back, and a checked exception commits it — with the one addition that keeps the outcome Spring’s, described in the note below. rollbackFor adds exception types that roll back, noRollbackFor adds types that commit, and when several listed types match the thrown one the most specific wins. The decision is written into the method as a chain of instanceof tests, so nothing reads the annotation when the method runs:

@Component
public class Orders {
    private final DataSource db;
    private final AuditLog audit;

    public Orders(DataSource db, AuditLog audit) {
        this.db = db;
        this.audit = audit;
    }

    @Transactional(rollbackFor = PaymentDeclined.class, timeout = 10)
    public long place(String sku, int quantity) throws IOException, PaymentDeclined {
        long id = db.insert("INSERT INTO orders (sku, quantity) VALUES (?, ?)",
                new Object[] {sku, Integer.valueOf(quantity)}, "id");
        audit.record("order " + id + " attempted");  // kept even if this rolls back
        charge(id);                                  // may throw PaymentDeclined
        return id;
    }

In Spring, a statement that fails throws the unchecked DataAccessException, so it rolls the transaction back. The methods of DataSource, Database and the daos declare the checked IOException, and the failures they report — a statement the engine refused, a connection that couldn’t be opened, a query that returned more than one row where one was expected — are com.codename1.backend.DataAccessException, a subclass of it. The default rule rolls back for that type as well, so a failed statement undoes the transaction as it would in Spring. Any other IOException — a file that couldn’t be read, a mail server that refused a message — is checked and commits, as it would in Spring; list it in rollbackFor to make it roll back, as the Signups example does. noRollbackFor = DataAccessException.class restores plain commit-on-checked.

A method that joined a transaction can’t roll back what the method that began it did before it, so when it fails with an exception that rolls back, it marks the whole transaction rollback-only instead. The method that began the transaction then rolls back when it ends. If it ends normally — because it caught the exception — the rollback is reported by throwing TransactionException.UnexpectedRollback, so the caller doesn’t take a rolled-back transaction for a committed one.

Transactions.setRollbackOnly() undoes a transaction without an exception. Called in the method that began the transaction, it lets that method return normally, and the transaction rolls back without an error when the method ends. Called in a method that joined the transaction, it counts as that method failing: the transaction becomes rollback-only, and the method that began it throws TransactionException.UnexpectedRollback when it ends normally. Called in a NESTED method, it undoes only that method’s work: its savepoint is rolled back when it returns, and the surrounding transaction carries on.

Transactions.isActive() and Transactions.isRollbackOnly() answer the obvious questions about the calling thread.

36.3.4. Read-only transactions and timeouts

readOnly = true begins a transaction the engine is told only reads. PostgreSQL and MySQL then refuse any write inside it, which turns a read path that writes by mistake into an error. SQLite accepts the flag and enforces nothing.

timeout is in seconds, counted from the start of the method. It’s checked when the transaction first touches the database, where a transaction already past its limit fails with an IOException, and again at commit, where it’s rolled back and reported with TransactionException.TimedOut. It doesn’t interrupt a statement that’s running, so it bounds how long a transaction can take to commit rather than how long any one query may run.

36.3.5. Savepoints

NESTED runs a method inside the open transaction, behind a savepoint. A failure rolls back to the savepoint and nothing more, and the outer method can go on and commit. That suits a batch in which one bad item shouldn’t cost the rest:

@Component
public class Imports {
    private final DataSource db;

    public Imports(DataSource db) {
        this.db = db;
    }

    @Transactional
    public int importAll(List<String> lines) {
        int imported = 0;
        for (String line : lines) {
            try {
                importLine(line);           // a call through this: still transactional
                imported++;
            } catch (Exception bad) {
                // Only this line's rows were rolled back, to its savepoint.
            }
        }
        return imported;                    // the good lines commit together
    }

    @Transactional(propagation = Propagation.NESTED)
    void importLine(String line) throws IOException {
        String[] fields = line.split(",");
        db.execute("INSERT INTO contact (name) VALUES (?)", new Object[] {fields[0]});
        db.execute("INSERT INTO phone (number) VALUES (?)", new Object[] {fields[1]});
    }
}

The loop calls importLine through this, which in Spring would bypass the annotation entirely and run every line in the outer transaction without a savepoint. Here the annotation is part of the method, so the call gets its savepoint however it’s made.

The savepoints are named by the runtime and released when the method returns normally. All three engines support them.

36.3.6. The transaction’s session

A bean can inject com.codename1.orm.session.Session, the persistence session of the ORM, and use it inside a transaction:

@Component
public class ReminderService {
    private final Session session;      // the current transaction's session

    public ReminderService(Session session) {
        this.session = session;
    }

    @Transactional
    public long remind(String title) {
        Reminder reminder = new Reminder();
        reminder.title = title;
        reminder.due = new Date();
        session.persist(reminder);      // written by the time the transaction commits
        return reminder.id;
    }

    @Transactional(readOnly = true)
    public Reminder find(long id) {
        return session.find(Reminder.class, Long.valueOf(id));
    }
}

The injected object stands in for the session of whichever transaction is open on the calling thread. The session is opened on the transaction’s connection the first time it’s used, its pending changes are flushed into the transaction before it commits, and it’s closed when the transaction ends. A savepoint that rolls back also clears the session, since the rows it had loaded since the savepoint no longer exist. Used outside a transaction, it throws with a message saying to annotate the method, or to open a session with EntityManager.openSession().

36.3.7. Why a call through this works

Spring applies @Transactional with a proxy: a wrapper object stands between a caller and the bean, and the transaction happens in the wrapper. A call that doesn’t go through the wrapper — one from the bean to itself, to a private method, or on an object the application built with new — gets no transaction, and nothing warns about it.

Here the build rewrites the compiled method. Its body moves to a method of its own and the method keeps its name, now starting the transaction, calling that body, and committing, with the rollback decision written in. The transaction is therefore a property of the method, and every call gets it. The same rewriting gives @Async, @Timed and @Counted the same property.

36.3.8. Pitfalls

  • Checked exceptions other than database failures commit. A failed statement rolls back, but an IOException from anything else — a file, a mail server, an outbound HTTP call — commits unless it’s listed in rollbackFor.

  • REQUIRES_NEW needs a second connection. The outer transaction keeps its connection while the inner one borrows another, so on a pool of one — the default for an in-memory SQLite database — the inner borrow waits for cn1.datasource.pool.borrowTimeoutMillis and fails.

  • SQLite has one writer. A transaction that has written holds the database’s write lock until it ends, so a REQUIRES_NEW method that writes while its caller holds the lock waits cn1.datasource.busyTimeoutMillis and fails. Reading works. On SQLite, record the audit row after the outer transaction, or in it.

  • Transactions don’t cross threads. Work handed to @Async or Tasks isn’t part of the caller’s transaction and can’t see its uncommitted rows.

  • A transaction holds a connection. From its first statement until it ends, the connection is out of the pool. An outbound HTTP call inside a transaction keeps it there for the length of the call, and enough of those at once starve the pool. Make the call before the transaction begins, or after it ends.

37. Backend sessions

HTTP is stateless: every request arrives on its own, and nothing in it says the client has been here before. A session is how a server remembers a client between requests. The server gives the client an unguessable id in a cookie, the client sends it back with every request, and the server finds the state it kept under that id. This chapter covers the session API, the cookie, where sessions are stored, and session-scoped beans.

The states of a session: no session, new, active and ended, with the change of id at sign-in and expiry after inactivity, above the two stores
Figure 249. A session from the first request to the last

37.1. Starting a session at sign-in

A handler reaches the session through the request:

@RestController
public class Login {
    @PostMapping("/login")
    public String login(HttpServer.Request request, @RequestParam("user") String user) {
        // ... check the credentials ...
        HttpSession session = request.getSession(true);
        session.changeSessionId();        // never keep an id from before the login
        session.setAttribute("user", user);
        return "ok";
    }
}

getSession(true) returns the client’s session, creating one if the request carried no valid cookie. Nothing is stored and no cookie is sent until something calls it, so a server that never uses sessions — an API authenticated with bearer tokens, say — pays nothing for them and sets no cookie. The server the build generates stores each session and sends its cookie when the request ends.

The call to changeSessionId() isn’t optional at sign-in. It gives the session a new id, keeps its attributes, and sends the client the new cookie. Without it a session id the client held before signing in stays valid after, and an attacker who planted that id in the victim’s browser beforehand — a session fixation attack — is signed in along with the victim.

37.2. Reading and ending a session

Most requests want the session only if there is one:

@GetMapping("/me")
public String me(HttpServer.Request request) {
    HttpSession session = request.getSession(false);   // never creates one
    Object user = session == null ? null : session.getAttribute("user");
    return user == null ? "nobody" : String.valueOf(user);
}

getSession(false) never creates a session, and answers null for a client without a valid one, so a read-only request doesn’t create a session as a side effect. invalidate() ends one, and the response clears the client’s cookie:

@PostMapping("/logout")
public String logout(HttpServer.Request request) {
    HttpSession session = request.getSession(false);
    if (session != null) {
        session.invalidate();          // the response clears the cookie
    }
    return "bye";
}

After invalidate(), the request no longer has that session: getSession(false) returns null, and getSession(true) starts a new one, whose cookie the response sends after clearing the old one.

HttpSession methodWhat it does

getAttribute(name), setAttribute(name, value), removeAttribute(name)

Read, set and remove the session’s state. Setting null removes the attribute.

getAttributeNames()

A copy of the attribute names.

changeSessionId()

A new id for the same session, with a new cookie. Call it at sign-in.

invalidate(), isValid()

End the session, and ask whether it has been ended. Reading or writing an attribute of an ended session throws IllegalStateException.

isNew()

Whether the current request created the session.

getMaxInactiveInterval(), setMaxInactiveInterval(seconds)

How long the session may go unused before it expires, for this session alone.

getCreationTime(), getLastAccessedTime()

When it was created and last used, in milliseconds since the epoch.

getId()

The id the cookie carries. It’s a credential, so never log it.

A session is saved after the handler returns, and only when the request changed it. A request that only reads its session writes nothing, apart from the last access time the database store keeps. That store updates it when it’s older than a minute, or a quarter of the session’s timeout when that’s shorter, and allows the same interval on top of the timeout before it calls a session expired, so a session in use is never expired early.

With the database store, two requests of one client load separate copies of the session. If one of them invalidates it, the other can’t write it back: a copy of a session whose row is gone is dropped rather than saved again, so a request still running with an old cookie can’t undo a sign-out.

The session id is 192 bits from a cryptographically secure random source, encoded for a cookie. The cookie is sent with Path=/ and HttpOnly, so scripts in a page can’t read it. SameSite=Lax keeps a browser from sending it with a form another site posts. On a TLS server it’s also Secure, so a browser never sends it over plain HTTP. It carries no Max-Age, so it lasts until the browser closes; how long the session lasts is the server’s decision.

KeyWhat it sets

cn1.session.cookie

The cookie’s name, CN1SESSION by default. It has to be a valid cookie name —  letters, digits and the punctuation HTTP allows in a token, such as -, _ and . — and anything else stops the server at start-up.

cn1.session.timeout

Seconds a session may go unused before it expires, 1800 by default. Zero means sessions never expire, and a negative value stops the server at start-up.

cn1.session.store

memory, the default, or db. See Section 37.4, “Where sessions are kept”.

cn1.session.same-site

Lax, Strict or None. None lets other sites send the cookie, and a browser accepts it only when it’s Secure, so the server refuses to start with None unless the cookie is.

cn1.session.secure

auto, the default, which marks the cookie Secure when the server terminates TLS; true for a server behind a proxy that terminates TLS for it; or false. Anything else stops the server at start-up.

37.4. Where sessions are kept

cn1.session.store=memory keeps sessions in the server’s own memory. It needs nothing, any object can be an attribute, and the object a request sets is the object the next one gets. It also means sessions end with the process, and that a second instance of the server doesn’t know about them, so behind a load balancer it needs the balancer to send each client to the same instance.

cn1.session.store=db keeps sessions in the server’s own database, in a table called cn1_http_session that the server creates on first use. Every instance of the server then sees every session, a restart loses nothing, and a deployment can add or replace instances at will. The price is in what an attribute can be: the attributes are stored as JSON, so a value must be a string, a number, a boolean, or a map or list of those. Anything else is written as JSON can write it and read back as whatever JSON reads, which isn’t the object that was stored.

Each row belongs to a namespace, and a server only reads the rows of its own. By default the namespace is empty, so every server that uses the database shares its sessions, as Spring Session’s JDBC store does. Browsers don’t keep cookies apart by port, so two different servers on one host that share a database would each accept the session cookie the browser sends to both, including a signed-in user’s. Give each project its own cn1.session.namespace to keep them apart; every instance of one project keeps the same one, so they still share sessions.

Either way, an expired session is removed at most once a minute, by the next request that asks for a session, and a request that presents an expired session’s cookie is treated as having no session at all.

37.5. Session-scoped beans

A bean marked @Scope("session") exists once per session, which suits state that belongs to a client and has behaviour of its own:

@Component
@Scope("session")
public class Cart {
    private final List<String> items = new ArrayList<String>();

    public synchronized int add(String sku) {
        items.add(sku);
        return items.size();
    }

    public synchronized List<String> items() {
        return new ArrayList<String>(items);
    }
}

A singleton that injects it receives a generated stand-in, and each call on the stand-in reaches the calling client’s own instance:

@RestController
public class CartApi {
    private final Cart cart;        // a stand-in: each call reaches the caller's own cart

    public CartApi(Cart cart) {
        this.cart = cart;
    }

    @PostMapping("/cart")
    public String add(@RequestParam("sku") String sku) {
        return String.valueOf(cart.add(sku));
    }

    @GetMapping("/cart")
    public List<String> list() {
        return cart.items();
    }
}

Using the stand-in starts a session if the client has none, as getSession(true) would. The instance is built on the session’s first use and kept by the server in memory for as long as the session lives. It’s never written to the session store, whichever store is configured, so with cn1.session.store=db each instance of the server builds its own copy for a client it serves. State that has to follow the client from one instance to another belongs in the session’s attributes instead.

The bean’s @PreDestroy method, or its destroyMethod for a @Bean method, runs when the session ends. That’s at the end of the request that invalidated it, when the periodic purge finds it expired, or when the server stops.

Two requests from the same client can run at once — two browser tabs, or an app that doesn’t wait for one answer before sending the next — and they reach the same instance. The methods of a session-scoped bean therefore need to be safe to call concurrently, which is why the Cart above synchronizes them.

A session-scoped bean can’t inject the request or the HttpSession: it outlives the request that built it, and with the database store each request loads its own copy of the session, so a copy held by the bean would go stale. The build refuses both. A method that needs the session reads the current one when it runs, with Backend.currentRequest().getSession(true).

37.6. Pitfalls

  • Skipping changeSessionId() at sign-in leaves the server open to session fixation. Call it whenever a session gains a privilege.

  • Storing objects the database store can’t write. A service, a stream or a domain object in an attribute works on the memory store and comes back as something else from the database store. Store ids and plain values, and load the rest per request.

  • Relying on memory sessions behind a load balancer without sticky routing makes a client appear signed out whenever a request lands on another instance.

  • Holding a lock across the whole session. Concurrent requests from one client share its attributes and its session-scoped beans, so a long synchronized method serializes that client’s requests. Keep critical sections short.

38. Backend scheduling and background work

Not everything a server does fits inside a request. A report takes a minute to build, webhooks go out after the response, a cleanup runs every night. This chapter covers the three ways to run work outside a request: @Async methods, which return before their work is done; @Scheduled methods, which run on a timetable; and Tasks, for a one-off piece of work. All three run on named executors, and each chooses between platform and virtual threads.

38.1. Background work

The annotations are Spring’s, on ordinary bean methods:

@Component
public class Reports {
    @Async
    public Future<String> monthly(String month) {
        return AsyncResult.of(render(month));
    }

    @Async(thread = ThreadKind.VIRTUAL)
    public void notifyWebhooks(String event) {
        // outbound calls that mostly wait
    }

    @Scheduled(cron = "0 0 3 * * *", zone = "Europe/Berlin")
    public void nightly() {
        // every day at 03:00 Berlin time
    }

    @Scheduled(fixedDelay = 60000, lock = "purge")
    public void purge() {
        // once a minute, on one instance at a time
    }

The build rewrites each @Async method so that it packages its arguments into a task, hands the task to an executor and returns. Each @Scheduled method is registered with the server’s scheduler, with a literal cron expression already compiled into bit masks. Neither involves a proxy, so both work however the method is called.

38.2. Asynchronous methods

An @Async method returns void or a java.util.concurrent.Future; any other return type is a build error, because the caller returns before there is anything to return. Future, the ExecutionException, TimeoutException and CancellationException its get() throws, and TimeUnit aren’t part of the client’s Java subset: they’re backend API, documented in the backend API reference and tested on the translated runtime. The body returns AsyncResult.of(value), and the caller receives, immediately, a Future that completes with that value when the body has run:

  • get() waits for the body and returns its value. On a virtual thread it yields its host while it waits rather than blocking it, so a request handler can wait for an @Async(thread = VIRTUAL) task without stalling the other connections that host serves.

  • An exception the body throws comes out of get() wrapped in an ExecutionException.

  • cancel() succeeds only before the body starts. A running body is never interrupted.

A void method has no Future to deliver a failure through, so an exception it throws is logged, counted against its executor, and recorded on the task’s span. The task runs in a span of its own whose parent is the caller’s, so background work shows up in a trace under the request that started it.

A synchronized @Async method holds its monitor while the body runs on the executor’s thread, not merely while it hands the task over, so two calls still never run the body at once.

@Async on a @Scheduled method is a build error: it would end the run as soon as the work was queued, so runs could overlap and a lock be released early; @Scheduled already runs off the request thread, on the thread its thread attribute picks. @Async on a request- or session-scoped bean builds with a warning, as it runs in Spring: the task calls the bean itself, which is destroyed when its request or session ends, possibly before the task runs.

Arguments are captured when the method is called, and the body runs later on another thread. Pass values rather than objects the caller goes on changing, and remember that the task runs outside the caller’s transaction and outside its request: a request-scoped bean isn’t available to it.

38.3. Executors

Background work runs on named executors, created on first use:

ExecutorUsed by

default

@Async methods that name no executor, and Tasks.platform.

default-virtual

@Async(thread = VIRTUAL) methods that name none, and Tasks.virtual.

scheduling, scheduling-virtual

@Scheduled methods that name none, by thread kind.

any other name

@Async("reports"), or @Scheduled(executor = "reports").

Each is configured under its own name:

cn1.task.executor.reports.threads=4       # pool size; 8 by default, 2 for scheduling
cn1.task.executor.reports.kind=platform   # platform or virtual; overrides the code

A pool’s threads start with its first task, so an unused executor costs nothing. A task submitted while every thread is busy waits in the executor’s queue, and cn1.task.queue_depth reports how many are waiting, by executor.

38.4. Platform or virtual threads

thread on @Async and @Scheduled chooses the kind of thread, from ThreadKind:

ValueRuns on

PLATFORM, the default

A thread of the executor’s pool.

VIRTUAL

A virtual thread of its own on the server’s hosts. Where there are none — the development run on the JVM, a TLS server, Windows — on the executor’s pool instead.

AUTO

A virtual thread where the server runs them, a platform thread otherwise. The choice is logged once at start-up.

VIRTUAL means ParparVM’s own virtual threads, which the native build has, and not the virtual threads of Java 21: the two behave differently, and the JVM development run has neither, so it runs VIRTUAL work on platform threads. The choice matters because of how virtual threads work on this runtime, explained under Section 32.6, “Virtual threads and the request loop”. A virtual thread parks while it waits on a socket, so work that queries PostgreSQL or MySQL or calls another service suits VIRTUAL well: thousands of such waits cost thousands of parked stacks rather than thousands of threads. What it must not do is block outside a socket. SQLite and file access are local calls that hold the host thread running them, and with it every connection that host serves, so work that uses them belongs on PLATFORM. PLATFORM stays the default, as a pool is in Spring.

Tasks runs a single piece of work without a bean method:

public void rebuildLater() {
    Tasks.platform(new Runnable() {
        public void run() {
            // runs once, on the default pool of platform threads
        }
    });
}

Tasks.virtual does the same on a virtual thread.

38.5. Scheduled jobs

A @Scheduled method takes no arguments and gives exactly one of cron, fixedRate or fixedDelay. Any of them can come from configuration:

@Component
public class Digest {
    @Scheduled(cron = "${digest.cron:0 0 7 * * MON-FRI}", zone = "America/New_York")
    public void weekdayMorning() {
        // 07:00 New York time on weekdays, unless digest.cron says otherwise
    }

    @Scheduled(fixedRateString = "${digest.pollMillis:30000}", initialDelay = 5000,
               executor = "polling", thread = ThreadKind.VIRTUAL)
    public void poll() {
        // every 30 seconds after a 5 second start-up delay, on a virtual thread
    }

38.5.1. Cron expressions

A cron expression has Spring’s six fields, seconds first, separated by spaces:

FieldValuesExamples

second

0-59

0, */10

minute

0-59

30, 0,15,30,45

hour

0-23

9-17, */2

day of month

1-31, or L for the last day

1, L, ?

month

1-12, or JAN to DEC

JAN,JUL, */3

day of week

0-7, or SUN to SAT; both 0 and 7 are Sunday

MON-FRI, ?

Each field is (or ? for the two day fields), a value, a range a-b, a step /n, a-b/n or a/n, or a comma-separated list of those. The macros @yearly (or @annually), @monthly, @weekly, @daily (or @midnight) and @hourly stand for the usual expressions.

When both day fields are restricted, a day has to match both. That’s Spring’s rule, and it differs from Unix cron, where matching either is enough: 0 0 12 13 * FRI fires on Friday the 13th and not on every Friday.

A literal expression is parsed by the build, so a mistake — 0 0 25 * * * names an hour that doesn’t exist — fails the build with the field at fault, rather than producing a job that never runs, or runs every second. An expression that reads configuration, ${digest.cron:…​} above, is parsed at start-up instead, and a bad one stops the server.

zone is the time zone the expression is read in: a zone ID such as Europe/Berlin, a fixed offset such as +02:00, or UTC. Without one it’s UTC, which is what a server’s clock should be set to regardless. The build checks the name, and a zone on a job that isn’t a cron job is an error. A zone that comes from configuration is checked when the schedule is made — against the host’s time-zone database where it has one — so a misspelled name stops the server instead of running the job on UTC without a word.

In a zone with daylight saving time, a time the clocks skip doesn’t exist, and a job due then doesn’t run that day: 0 30 2 * * * in America/New_York skips the spring-forward night instead of running at 03:30. A time the clocks pass twice, on the night they go back, runs once, at the first of the two. Once that has passed the day’s run is spent, so a server started inside the repeated hour doesn’t run it that night.

38.5.2. Fixed rate and fixed delay

fixedRate starts runs a fixed number of milliseconds apart, measured from start to start. fixedDelay waits that long after each run ends. initialDelay sets how long after start-up the first run begins; without it the first run starts as soon as the server does. The String forms, fixedRateString, fixedDelayString and initialDelayString, accept a placeholder, so a period can be configured per deployment.

Three timelines of the same job: fixedRate starting every ten seconds, fixedDelay waiting ten seconds after each end, and a slow run that causes two skipped starts
Figure 250. fixedRate, fixedDelay and a run that overruns

38.5.3. Runs never overlap

A run never overlaps the previous run of the same job. A start that finds the previous run still going is skipped, and the job is next due at its following time; it doesn’t queue up and run twice to catch up. A fixed-rate job that falls behind resumes on its own grid, without a burst. Each skipped start is counted, so a job that routinely overruns its period shows up in the job listing rather than running less often than it says with nobody noticing.

The scheduler itself is one platform thread that keeps the timetable. The runs happen on the executors, so a slow job delays nothing but its own next run.

38.5.4. One instance at a time

With several instances of a server behind a load balancer, each runs every job. For a job that should happen once — sending the daily digest, purging old rows — lock names a row it claims first:

@Scheduled(fixedDelay = 60000, lock = "purge")

Before each run the instance tries to claim the named row in a table called cn1_scheduler_lock, in the server’s own database, and skips the run when another instance holds it. The claim expires after lockAtMostFor milliseconds — ten minutes when it isn’t set — so an instance that dies mid-run doesn’t hold the job forever. Set it longer than the job’s longest run, or two instances can end up running it at once. The claim’s times come from the database server’s clock, which every instance shares, so an instance whose own clock runs ahead can’t take a claim another still holds. With SQLite, which runs in the server’s process, it’s that host’s clock. A job with a lock needs a database, which the build checks.

A skipped run isn’t a failure. A lock that can’t be claimed for any other reason — the database refusing the statement — is recorded as one, so a broken lock table doesn’t look like a job that’s always somebody else’s turn.

38.5.5. Watching the jobs

A job is named after its class’s simple name and its method, Cleanup.run. When two classes in different packages share a simple name, both jobs take the qualified name, com.example.billing.Cleanup.run, so each can be listed and started on its own.

GET /manage/jobs lists every job with its schedule, its runs, failures and skipped starts, when it last started, how long that took, the last error, and when it’s next due. The development MCP server has the same as backend_jobs, and backend_run_job starts one immediately. The duration of each run is recorded in the cn1.scheduler.run.duration histogram, by job and outcome.

When the server stops, the scheduler stops first so that no run begins during the drain, and runs that haven’t finished get the same grace period as the requests in flight. A task on a virtual thread keeps running on its host thread through that window. One still running when it ends is abandoned and reported, and its Future never completes.

38.6. Pitfalls

  • SQLite or file work on a virtual thread blocks the host thread under it, because neither waits on a socket. Leave such work on PLATFORM. A PostgreSQL or MySQL query parks instead, and is fine on VIRTUAL.

  • Relying on the caller’s context. An @Async body runs outside the caller’s transaction and request. A transaction it needs, it declares itself.

  • A cron expression read in local time. Without zone it’s UTC, so 0 0 9 * * * fires at 09:00 UTC, which is the middle of the night somewhere.

  • lockAtMostFor shorter than a run. Once the claim expires another instance may start the job while the first is still running it.

  • Several instances without lock. Every instance runs every job.

39. Backend tracing, metrics and management

A server in production has to answer three questions for whoever runs it: what did this request do, how’s the server doing, and what can be changed without redeploying. Traces answer the first, metrics the second, and managed beans the third. All three follow the OpenTelemetry conventions, so they reach whatever observability stack a deployment already runs, and all three are linked into the binary only when the build is asked for them.

39.1. Tracing with OpenTelemetry

A server can report every request it serves to any OpenTelemetry collector, and the reporting needs no code in the handlers. Turn it on at build time with the annotation, on any class in the backend module:

@OpenTelemetry(serviceName = "notes")
@RestController
public static class NotesController {
    @GetMapping("/notes/{id}")
    public String note(@PathVariable("id") String id) {
        return id;
    }
}

Without touching the source, set it in application.properties instead:

cn1.otel.enabled=true

Either one makes the generated entry point install a tracer. From then on:

  • every request is a server span, named after the route that matched (GET /notes/{id}), with its status, method and path;

  • every outbound Web call is a client span, and sends the W3C traceparent and tracestate headers so the service it reaches joins the same trace;

  • every Database statement is a client span carrying the SQL as the code wrote it, placeholders and all — the bound values are never recorded;

  • an incoming traceparent makes the request part of the caller’s trace, which is how a Codename One app’s spans connect to the backend’s own (see Chapter 75, Distributed tracing with OpenTelemetry).

A WebSocket connection isn’t a span. It can stay open for hours and carry any number of messages, so it has no single start and end to time, and its upgrade request is answered before tracing begins.

Without the annotation or the property, nothing refers to the tracer and the translator leaves it out of the binary.

Spans go out over OTLP/HTTP, as binary protobuf by default, batched on a thread of their own so a slow collector never holds up a request. A queue that fills because the collector is down drops spans and counts them, and HttpServer.getMetrics() reports those counts. The settings are the standard OpenTelemetry environment variables, so a deployment configures this server the same way it configures everything else it runs:

VariablePropertyMeaning

OTEL_EXPORTER_OTLP_ENDPOINT

cn1.otel.endpoint

The collector’s base URL; /v1/traces is appended. Defaults to http://localhost:4318.

OTEL_EXPORTER_OTLP_TRACES_ENDPOINT

cn1.otel.traces.endpoint

The full URL, used as it is.

OTEL_EXPORTER_OTLP_HEADERS

cn1.otel.headers

name=value pairs, comma separated, with each value URL-encoded.

OTEL_EXPORTER_OTLP_TRACES_HEADERS

cn1.otel.traces.headers

Headers for traces only, in place of the shared ones.

OTEL_EXPORTER_OTLP_PROTOCOL

cn1.otel.protocol

http/protobuf or http/json. gRPC isn’t supported.

OTEL_EXPORTER_OTLP_TRACES_PROTOCOL

cn1.otel.traces.protocol

The protocol for traces only.

OTEL_SERVICE_NAME

cn1.otel.service.name

Overrides the annotation’s serviceName.

OTEL_RESOURCE_ATTRIBUTES

cn1.otel.resource.attributes

Extra resource attributes, key=value pairs.

OTEL_TRACES_SAMPLER

cn1.otel.sampler

parentbased_always_on by default; also always_on, always_off, traceidratio and the other parentbased_ forms.

OTEL_TRACES_SAMPLER_ARG

cn1.otel.sampler.arg

The ratio for the ratio samplers.

OTEL_SDK_DISABLED

cn1.otel.disabled

true turns tracing off at start-up without a rebuild.

OTEL_BSP_MAX_QUEUE_SIZE

cn1.otel.queue.size

Spans held for export before new ones are dropped. Defaults to 2048.

OTEL_BSP_MAX_EXPORT_BATCH_SIZE

cn1.otel.batch.size

Spans sent in one export. Defaults to 512.

OTEL_BSP_SCHEDULE_DELAY

cn1.otel.export.delayMillis

Milliseconds between exports. Defaults to 5000.

cn1.otel.attributes.exclude names attributes never to record, for example db.query.text in a code base that builds SQL by concatenating values.

Sending spans to Dynatrace, which accepts OTLP over HTTP as protobuf, is a matter of pointing the endpoint at the environment’s OTLP API and passing an ingest token:

OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://abc12345.live.dynatrace.com/api/v2/otlp/v1/traces
OTEL_EXPORTER_OTLP_HEADERS=Authorization=Api-Token%20dt0c01.XXXX

Under Lambda, the invocation continues the trace the host passes in its X-Ray header, and the loop waits for the export before it polls again, because the host freezes the process between invocations.

Tracing.current() returns the request’s span, for adding an attribute of the application’s own, and Tracing.inSpan times a block of work as a child span.

39.1.1. Relaying the app’s spans

A mobile app shouldn’t carry the collector’s credentials: anything inside an app package can be read by whoever installs it. With cn1.otel.relay=true the backend accepts the app’s spans at /otel/v1/traces (cn1.otel.relay.path moves it), adds its own credentials and forwards them to the same collector:

cn1.otel.relay=true
# Optional: a shared secret the app sends in X-CN1-Telemetry-Token.
cn1.otel.relay.token=${RELAY_TOKEN}
# Only for a web app served from another origin.
cn1.otel.relay.corsOrigin=https://app.example.com

The relay takes OTLP/JSON, rebuilds it against the OTLP schema — a field the schema doesn’t name is dropped and a malformed id is refused — and queues it for export in whatever protocol the server exports with. It answers as soon as the spans are queued, and answers 503 when the queue is full so the app backs off. cn1.otel.relay.maxBytes and cn1.otel.relay.maxSpans bound a single export.

39.2. Metrics and management

Traces say what one request did; metrics say how the server is doing. With OpenTelemetry turned on, the server exports metrics over OTLP on the same endpoint and with the same settings as its traces, every minute by default, with cumulative temporality. cn1.otel.metrics.enabled=false turns metrics off while leaving traces on. The management endpoints described below record and serve the same metrics whether anything exports them or not.

39.2.1. What the server records

InstrumentKindAttributes

http.server.request.duration, in milliseconds

histogram

http.route (the route template), http.request.method, http.response.status_code

http.server.active_requests

gauge

http.server.open_connections

gauge

cn1.server.websocket_connections

gauge

cn1.server.requests_served, cn1.server.connections_refused

gauge

db.client.connection.count, db.client.connection.idle

gauge

(when the server has a database)

cn1.task.queue_depth

gauge

cn1.executor

cn1.scheduler.run.duration, in milliseconds

histogram

cn1.job, cn1.outcome (success or failure)

process.runtime.memory.used, process.uptime

gauge

The route attribute is the template — GET /notes/{id} is recorded as /notes/{id} — so a server’s metric count doesn’t grow with the ids its clients ask for. A request that fails, whether in a handler or in the session store, is recorded with status 500.

39.2.2. Managed beans

The application’s own metrics follow the JMX model — attributes to watch and operations to call — with Spring’s annotations for it:

@Component
@ManagedResource(objectName = "prices")
public class PriceCache {
    private final Map<String, Double> prices = new LinkedHashMap<String, Double>();

    @ManagedAttribute(description = "Cached prices", unit = "{entry}")
    public synchronized int getSize() {
        return prices.size();
    }

    @ManagedOperation(description = "Drops every cached price")
    public synchronized void clear() {
        prices.clear();
    }

    @Timed
    @Counted
    public synchronized Double price(String sku) {
        return prices.get(sku);
    }
}

@ManagedResource marks the bean, and objectName names it; without one it’s the simple class name. The name is a segment of the management URL, so it’s made of letters, digits, _, - and . only. Each @ManagedAttribute is an instance getter with no parameters, and a numeric one becomes a gauge named after the resource and the attribute — prices.size here — read each time metrics are collected. Every attribute, numeric or not, appears in the management listing. A @ManagedOperation is a method an operator can call, whose parameters may be strings, numbers, booleans, and enums. An operation’s parameters are named by @McpParam when they carry one, and otherwise arg0, arg1, in order. The build checks each of these rules and refuses a managed method on a class with no @ManagedResource. It also refuses a managed resource that isn’t a singleton, because metrics are read outside any request or session. An operation is invoked by name, so it refuses an overloaded one too.

@Timed records each call’s duration as a histogram, and @Counted counts calls, with a second counter for the failures. Their instruments are named after the class and method unless the annotation’s value names them:

AnnotationInstruments

@Timed

<class>.<method>.duration, a histogram in milliseconds

@Counted

<class>.<method>.calls, and <class>.<method>.calls.failures for calls that threw

<class> is the fully qualified class name. Like @Transactional, they’re woven into the method at build time, so a call through this is measured too.

39.2.3. Instruments of your own

Metrics.counter, Metrics.histogram and Metrics.gauge create an instrument for anything the annotations don’t cover:

public final class OrderMetrics {
    private static final Counter PLACED =
            Metrics.counter("orders.placed", "Orders placed", "{order}");
    private static final Histogram TOTALS =
            Metrics.histogram("orders.total", "Order totals", "USD");
    private static final List<String> QUEUE = new ArrayList<String>();

    static {
        Metrics.gauge("orders.queued", "Orders waiting to ship", "{order}",
                new Gauge.Source() {
                    public double read() {           // read when metrics are collected
                        synchronized (QUEUE) {
                            return QUEUE.size();
                        }
                    }
                });
    }

    public static void placed(String id, double total) {
        PLACED.increment();
        TOTALS.record(total);
        synchronized (QUEUE) {
            QUEUE.add(id);
        }
    }

Asking for the same name twice returns the same instrument, so two classes may declare it, and asking for an existing name as a different kind is refused. Keep instruments in static fields, so recording a value looks nothing up. A gauge is a callback read when metrics are collected, which suits a value that already exists somewhere, such as a queue’s length. A histogram’s second form takes bucket boundaries and up to three attribute keys, recorded with record(value, first, second, third).

39.2.4. Exporting

Metrics use the tracing settings from Section 39.1, “Tracing with OpenTelemetry” unless a metrics-specific one is set, which is the precedence the OpenTelemetry specification gives:

VariablePropertyMeaning

OTEL_METRIC_EXPORT_INTERVAL

cn1.otel.metrics.intervalMillis

How often to export, in milliseconds. A minute by default.

OTEL_EXPORTER_OTLP_METRICS_ENDPOINT

cn1.otel.metrics.endpoint

The full URL. By default the tracing endpoint’s base with /v1/metrics appended.

OTEL_EXPORTER_OTLP_METRICS_HEADERS

cn1.otel.metrics.headers

Headers for the metrics requests; by default the tracing ones.

OTEL_EXPORTER_OTLP_METRICS_PROTOCOL

cn1.otel.metrics.protocol

http/protobuf or http/json; by default the tracing protocol.

cn1.otel.metrics.enabled

false exports traces only.

When the server stops, it exports once more so the last interval isn’t lost, bounded by the shutdown timeout.

39.2.5. Management endpoints

The management endpoints make the same information available over HTTP, for a load balancer and for an operator:

GET  /manage/health        public; 503 while starting or draining
GET  /manage/metrics       every metric, as JSON
GET  /manage/prometheus    every metric, in the Prometheus text format
GET  /manage/jobs          the scheduled jobs and their last runs
GET  /manage/managed       the managed beans and their attributes
POST /manage/managed/{bean}/{operation}

An operation’s body is a JSON object of its arguments by parameter name. A body that isn’t one, or an argument the operation refuses, is answered with 400; 404 means only that no managed bean or operation has that name. Two managed resources with the same objectName, or two @McpTool methods with the same name, are a build error, since each is found by that name.

Health reports STARTING until the server has finished starting, including the application’s scheduled jobs and exporters, UP while it serves, and DRAINING once it’s stopping. The listener accepts connections before start-up finishes, so a load balancer that checks health doesn’t send traffic to a server that may still fail to start.

KeyWhat it sets

cn1.management.enabled

Whether the endpoints are served. On by default on a development profile, off everywhere else.

cn1.management.token

The bearer token every endpoint but health requires. Outside a development profile the server refuses to start with the endpoints on and no token, since the metrics and managed beans would be readable by anyone who can reach the port.

cn1.management.path

Where the endpoints live, /manage by default.

A packaged server has no management code at all unless its build asks for it. The generated entry point is the only code that names the endpoints, and it names them only when the build sees cn1.management.enabled=true in application.properties or a profile’s file. Without it, the translator leaves the endpoints out of the binary, so there’s no /manage to find or to misconfigure, whatever profile the server later runs under. The development run through cn1:backend always builds them in.
cn1.management.enabled=true
cn1.management.path=/ops

A server built with them can still turn them off at start-up with cn1.management.enabled=false, and one built without them ignores the key, because there’s nothing to turn on.

On a development profile without a token, the read-only views are open to anyone who can reach the port, which is a laptop. An operation needs the token on every profile, because it changes the running server.

Health answers 503 while the server drains, so a load balancer stops sending it new requests before it stops accepting them. The Prometheus view turns each name into a Prometheus one, replacing the dots with underscores and adding _total to a counter, so http.server.request.duration is scraped as http_server_request_duration.

The management endpoints are served before the application’s routes, so a catch-all route can’t answer them.

40. Backend MCP and the agent loop

The Model Context Protocol lets an AI agent call tools on a server. A Codename One backend serves it at /mcp, for two different readers. In production the agent is a user of the application, and the tools are the ones the application chooses to publish. In development the agent is the one writing the server, and the tools let it look inside the running server and exercise it, the way a developer uses a debugger and a database console.

40.1. Serving MCP

The first reason is the application’s own tools. An @McpTool method becomes a tool an agent can call, with its JSON Schema written by the build from the parameter types:

@Component
public class SupportTools {
    private final Signups signups;

    public SupportTools(Signups signups) {
        this.signups = signups;
    }

    @McpTool(description = "Counts the people who signed up. Use it before "
            + "answering a question about growth.")
    public int signupCount() throws Exception {
        return signups.count();
    }

    @McpTool(description = "Signs someone up, as the form on the site would.")
    public String signUp(@McpParam(value = "email", description = "Their email address")
                         String email) throws Exception {
        signups.register(email);
        return "signed up";
    }
}

The description is what an agent chooses tools by, so it says when to use the tool as well as what it does. A parameter needs @McpParam to name it, since a Java parameter name doesn’t survive compilation. Outside a development profile the endpoint refuses to start without cn1.mcp.token. On a development profile the token is optional, but a server without one listens on 127.0.0.1 only, since its tools reach the database and every handler; set a token to serve other machines, and a server bound to another address refuses to start without one. It also refuses a browser request unless its origin is a loopback address or is listed in cn1.mcp.allowedOrigins, which keeps a web page from driving it. A page the server serves itself has to be listed too. An allowed origin gets the CORS answers a browser needs, the preflight included.

40.1.1. Writing a tool

A tool is an instance method of a bean. Its name is the method’s name unless @McpTool(name = …​) gives one, and a name is letters, digits, _, - and . only. Each parameter carries @McpParam with the argument’s name, which has to be unique within the tool, an optional description that goes into the schema, and required, true by default. A tool can’t also be @Async: the agent gets what the method returns, so the method has to run to the end. A parameter may be a String, a number, a boolean, an enum, a Map or a List; the build writes the matching JSON Schema type, and refuses any other type. A number that doesn’t fit the parameter — too large for an int, or for a float — is refused rather than narrowed.

When an agent calls the tool, the arguments are converted to the parameter types and the method is called directly, with no reflection. A value that doesn’t fit — text where a number belongs, or a number outside the range of a short — is answered as a tool error rather than converted into something the agent didn’t send. The return value is sent as the call’s text: a String unchanged, anything else as JSON.

A method that throws is answered with a result marked as an error, which an agent reads and can correct, rather than with a protocol failure. An IllegalArgumentException sends its message, so that’s the exception to throw for arguments the tool can’t use, with a message saying what would work.

40.1.2. The endpoint

The endpoint speaks JSON-RPC 2.0 over MCP’s Streamable HTTP transport, and negotiates protocol versions 2025-06-18, 2025-03-26 and 2024-11-05. It answers initialize, ping, tools/list and tools/call, and answers the resource and prompt listings with empty lists. It keeps no session and opens no event stream, which the transport allows, so each call is one POST with one JSON answer and a GET is answered 405.

KeyWhat it sets

cn1.mcp.enabled

Whether the endpoint is served. On by default when the application has tools, or when the development tools are available; set false to turn it off.

cn1.mcp.token

A bearer token every call must carry. Required outside a development profile; without it a development server listens on 127.0.0.1 only.

cn1.mcp.allowedOrigins

Comma-separated browser origins allowed besides loopback ones, or *.

cn1.mcp.path

Where the endpoint lives, /mcp by default.

cn1.mcp.devTools

Whether a development build serves the development tools. On by default on a development profile.

A server restarted in a test serves the tools its own beans provide and nothing a previous one registered.

A packaged server has no MCP code at all unless its build asks for it: a class with an @McpTool method, or cn1.mcp.enabled, cn1.mcp.path or cn1.mcp.allowedOrigins in a properties file. Without any of those, the generated entry point never names the endpoint and the translator leaves it out of the binary, development tools included; cn1:backend builds the development tools in for the development run only. A server built with the endpoint turns it off at start-up with cn1.mcp.enabled=false.

40.2. Tools for developing the server

The second reason is development. When the server runs through cn1:backend on a development profile, the same endpoint also serves tools for inspecting and exercising it, and the server prints the endpoint’s address when it starts:

ToolUse it to

backend_routes

List every route: method, path and the controller method behind it.

backend_beans

List every bean, its scope and what was injected into it, and which conditional beans are active.

backend_config

See the active profile and every configured key, with values that look secret masked.

backend_call

Send the server a request — method, path, body, headers — and read the status, headers and body.

backend_requests

See the last requests served, newest first, with status and time; failuresOnly shows only the ones that failed, with the exception each threw.

backend_logs

Read the last lines the server printed.

backend_sql

Run SQL against the server’s database. Unless write is true, the statement runs in a read-only transaction the database enforces, one statement at a time, so a statement that writes fails even when it begins like a read.

backend_schema

List the entities and their tables and columns.

backend_jobs, backend_run_job

List the scheduled jobs; start one now.

backend_metrics

Read every metric’s current value, optionally only those with a prefix.

backend_managed, backend_invoke

List the managed beans; call an operation.

Claude Code connects to the endpoint with one command:

claude mcp add --transport http cn1-backend http://127.0.0.1:8080/mcp

A host that only speaks stdio runs com.codename1.backend.mcp.StdioBridge from the backend jar with that address instead, and the bridge relays each message. None of these tools are compiled into a server built with cn1:backend-package, unless -Dcn1.backend.devTools=true asks for them. A Gradle project’s backendPackage task takes the same switch as -Pcn1.backend.devTools=true.

40.3. The loop an agent works in

The development tools are meant to be used in a loop that checks each change against the running server rather than against the code alone:

  1. Start the server on the development profile. It gets an in-memory SQLite database with the entity tables created, so it needs nothing installed:

    CN1_PROFILE=dev mvn -pl backend -Dcodename1.platform=backend cn1:backend
  2. Learn the server with backend_routes, backend_beans and backend_schema.

  3. Change the code, restart the server, and exercise the endpoint with backend_call before touching any UI. A failing endpoint is much cheaper to diagnose here than through a screen.

  4. Check what the server saw with backend_requests, and what it stored with backend_sql.

  5. For a change that spans the app as well, run the app in the simulator and drive it through the simulator’s own MCP server, described in Chapter 64, MCP Headless API, then check the requests and rows the same way.

There is no hot reload: after a change to backend code, stop the server and start it again. That takes seconds, and the build regenerates the wiring on the way.

41. Backend performance, deployment and limits

This chapter is for deciding whether the backend fits a service, and for putting it into production once it does: what it costs in time and memory compared with the alternatives, how the binary ships, and the limits worth knowing first.

41.1. What it costs

Same handler, three ways, plus Go for an outside reference. Two pinned cores, 64 connections, medians of three interleaved runs on the plaintext route:

RuntimeRequests/secp50p99Cold start

Codename One native, musl

595,610

0.090 ms

0.249 ms

0.77 ms

Codename One native, glibc

547,761

0.065 ms

4.06 ms

2.88 ms

Go, fasthttp

496,293

0.104 ms

2.63 ms

2.39 ms

The same handler on the JVM

187,745

0.260 ms

1.60 ms

82.5 ms

Cold start is the interesting column. It’s measured from process spawn to the first accepted connection, and the static binary reaches it in under a millisecond — about a hundred times faster than the same code on a JVM, and three times faster than Go. That number is the whole serverless argument.

Throughput is the least interesting one. Beating a tuned Go server by a fifth on a microbenchmark isn’t a reason to move a service; it’s only evidence that the translation doesn’t cost you anything.

A slope chart showing that the musl build’s tail stays close to its median while the others fan out
Figure 251. Latency at the median against the 99th percentile

The slope chart is the one that matters. Every runtime here has a similar median. What differs is the distance to the 99th percentile, and that distance is garbage collection. With the response pooled the plaintext route allocates about 0.1 bytes per request, the collector never runs, and the tail stays at 2.8 times the median. Give the same server a handler that allocates a map per request and its tail goes to 80 ms, because the collector shares the cores with the server.

The honest rule is that the tail follows your allocation rate, not the runtime badge. The runtime gives you the tools to allocate nothing on the hot path; it doesn’t do it for you.

41.1.1. Memory and size

RuntimeBinary or artifactResident under load

Codename One native, musl

7.95 MB static

10-40 MB

Codename One native, glibc

3.19 MB dynamic

14 MB

Go, fasthttp

5.63 MB static

6.3 MB

The same handler on the JVM

0.13 MB jar, plus a JRE

190 MB

The JVM row is the same handler and the same protocol code. Everything it costs above the native rows is the runtime underneath it.

The resident figures move around more than the latency ones, because the collector keeps a pool of pages sized to the busiest moment the process has seen and gives them back gradually. At rest the native builds sit near 3 MB.

41.1.2. musl or glibc

Both are supported, and they aren’t equivalent. The static musl build starts faster and has a far shorter tail. The glibc build has a better median, because its allocator is better under contention, and a smaller binary, because it links the system libraries instead of carrying them.

The reason to pick musl isn’t the median. It’s that the artifact is one file with nothing underneath it, which is what makes the container the binary and the cold start a process exec.

41.1.3. What isn’t measured here

GraalVM is missing from these tables on purpose. A fair comparison would have to run the same handler, and this handler can’t run on GraalVM: the runtime’s native methods are ParparVM’s, so comparing would mean benchmarking a different server written against a different framework and reporting it as though the toolchains had been compared. That’s a benchmark worth building, and it isn’t this one.

41.2. Deploying it

The musl build is a single static file, so the container that carries it can be empty:

FROM scratch
COPY bench-linux-musl-arm64 /server
ENTRYPOINT ["/server"]

There is no base image to patch, because there is no base image. cn1:backend-package builds for the machine it runs on; the cross-compiled targets (musl-x86_64, musl-arm64, glibc-x86_64, glibc-arm64) are produced by vm/backend/package.sh in the Codename One repository, which drives one builder image per target.

For AWS Lambda, LambdaRuntime implements the custom runtime loop. The Lambda Runtime API is a plaintext poll over loopback, so it needs no listening socket and no TLS, and what it does need is exactly what a translated binary is good at.

41.3. Limits worth knowing

  • The class library is the Codename One runtime, not Java SE. A server dependency that assumes the full JDK won’t translate, and Maven Central isn’t the ecosystem this draws on.

  • Beans, injection, transactions, scheduling and metrics are resolved at build time, so what a runtime container discovers on its own — beans in a jar found by scanning, a proxy created for a class decided at run time — isn’t available. There is no starter ecosystem, and validation and error mapping are written by hand or generated from the REST contract.

  • The packaging goal compiles Java. It recompiles the module’s sources against the backend’s class library instead of reusing the jar Maven built, which is what keeps a server off classes the runtime doesn’t have, and is also why Kotlin is not wired into this path yet even though the client ports support it. It uses the JDK running Maven — any release from 8 up — and -Dcn1.backend.jdk names a different one. What it can’t change is the language level: the module’s sources are compiled at Java 8, because that’s the bytecode the translator reads.

  • A virtual thread parks on sockets and on nothing else. A PostgreSQL or MySQL query, a Web call and a TLS handshake all park it, and its host serves other connections meanwhile. SQLite, file access, resolving a host name and Object.wait() block the host thread that’s running them, and with one host per core, that many concurrent slow calls of that kind occupy every host while other connections wait. A server whose handlers spend their time in SQLite should size for that, or run the thread pool with CN1_HTTP_POLL_MODE=0.

  • TLS runs on the thread pool, whatever the poll mode says. The TLS layer can’t park a read yet, and a blocking read on a virtual thread holds its host for the duration, so one idle TLS client per core would occupy every one of them. The server says so at startup when it makes that choice.

  • Native builds target Linux. Development happens anywhere a JVM runs.

  • WebSockets are RFC 6455 over HTTP/1.1. There is no permessage-deflate, so messages go out uncompressed no matter how compressible, and no RFC 8441, so a browser that reaches this server over HTTP/2 opens a second connection for its WebSocket rather than carrying it on the first. Both are what every client already falls back to. wss works on the packaged runtime through the same TLS the rest of the server uses, and not in the local loop, which terminates no TLS.

The reasons to choose this are cold start, footprint, deployment shape, and one language across the app and its server. If none of those matter for the service in front of you, use a JVM framework.

Device and platform services

42. Push notifications

For new integrations, Codename One push uses a typed message envelope and an explicit client binding. Your main application class doesn’t implement a push interface. Existing applications using PushCallback can keep that integration; see Migrate an existing push server before changing their payload format. The same application code receives messages from Android, iOS, Huawei devices, and supported web browsers.

You can use the managed BuildCloud service or install an adapter for custom native push. A custom-provider adapter is a low-level escape hatch: it bypasses BuildCloud registration and delivery completely, so your server owns tokens, credentials, targeting, retries, and provider errors.

That choice reroutes both halves of the journey, registration and delivery, and leaves the application code alone. The rest of this chapter fills in each box.

How registration and delivery flow in managed and custom provider mode
Figure 252. Registration and delivery, managed and custom

42.1. Create and configure an application

Open the Codename One Console in the organization that owns the application. Create one Push application for each logical application and environment. Keep production and staging separate so credentials, audiences, and analytics remain isolated.

  1. Open Push > Settings. Enter a name under Applications and select Create application.

  2. Use the Selected application picker at the top of the Push page to select that application. Settings can also list applications registered by the classic API. If the application isn’t among those loaded, use Load more in Settings, then return to the picker.

  3. Open the Android or iOS provider tab under Native push providers, paste the credential JSON described below, and save it.

  4. Check Configured in Settings or Ready under Push > Overview > Channel readiness for the same selected application. These labels mean a credential is stored; a real test send is still needed to verify provider authentication and device delivery.

Use the following identifiers for their separate purposes:

ValueWhere to find itWhere to use it

Application UUID

Push > Overview > Server API > Copy ID

The appId field in a V3 server send. It isn’t the mobile package name, Apple bundle ID, or client binding key.

Client binding key

Push > Overview > Client binding

PushClient.builder() and client registration. It can’t authorize a server send.

Server API key

Account > Server API keys

Authorization: Bearer or X-API-Key on your server. Create it with the push scope; the classic push token is a separate credential.

Select any readiness card, including a green Ready card, to open Settings on that provider’s tab.

Push Overview with client binding and provider readiness cards
Figure 253. Push Overview shows the client binding and provider readiness cards

42.1.1. Android: Firebase service-account JSON

  1. In the Firebase project used by the installed Android app, open Project settings > Service accounts and select Generate new private key.

  2. Open the downloaded service-account JSON file locally and copy the complete JSON object.

  3. In Push > Settings > Native push providers > Android, paste it into Credential JSON and select Save Firebase Cloud Messaging credential.

The server credential contains project_id, client_email, and private_key. Android’s google-services.json configures the mobile app and can’t authenticate server sends. Keep the service-account private key on the server; don’t bundle it into the app or paste it into a support thread.

42.1.2. iOS: APNs key and credential JSON

Create an APNs authentication key in Apple Developer Certificates, Identifiers & Profiles. Download its .p8 file and note the key ID and your Apple team ID. Use the bundle ID of the installed app as topic.

The Console’s Credential JSON field expects a JSON object containing those values. It doesn’t accept the raw .p8 file by itself. Replace the placeholders in this example:

{
  "teamId": "YOUR_APPLE_TEAM_ID",
  "keyId": "YOUR_APNS_KEY_ID",
  "topic": "com.example.app",
  "environment": "production",
  "privateKey": "-----BEGIN PRIVATE KEY-----\nYOUR_COMPLETE_P8_KEY_CONTENTS\n-----END PRIVATE KEY-----\n"
}

privateKey contains the entire .p8 text, including the BEGIN/END lines. Within the JSON string, encode each line break as \n; literal line breaks inside a quoted JSON value are invalid. Use environment: "production" for App Store and TestFlight installations, and "sandbox" for development-signed installations. The bundle ID, Apple team, environment, and device token must belong to the same app installation.

In Push > Settings > Native push providers > iOS, paste the completed object and select Save Apple Push Notification service credential. Token authentication uses the APNs signing key instead of an expiring push certificate.

42.1.3. Other providers and saved status

  • Huawei: save the AppGallery Connect application ID and client secret. Add agconnect-services.json to the project.

  • Windows / WNS: save the Package SID and client secret associated with the Store identity.

  • Web Push: save the VAPID public and private keys and subject.

The managed backend includes WNS delivery for explicit Windows targets. It doesn’t revive or generate bindings for the unsupported UWP port.

Credentials are encrypted in the BuildCloud secrets vault and are write-only. The input is cleared after saving and stays blank when you return to the page. Check the saved-status label for the same selected application instead of expecting the secret to reappear. If the status says Not configured or Status unavailable, check the selected application and organization and report any error. Rotate a credential by saving its replacement.

42.1.4. Prepare a new client build

Cloud builds detect PushClient usage and generate the typed native bootstrap automatically. On Android, include google-services.json, agconnect-services.json, or both. A build with both configurations uses FCM when Google Play Services are available on the device and falls back to Huawei Push Kit otherwise. No provider-selection build hint is required. The android.messagingService=fcm and android.messagingService=huawei hints remain available only when an application intentionally needs to force one provider.

iOS enables push support automatically when PushClient is referenced; set ios.includePush=false only when an installed custom-provider adapter intentionally owns registration.

The Console displays an application key. This key identifies an application during device registration; it isn’t a server API key and doesn’t authorize sending messages. Keep send API keys on your server.

Push Settings with platform tabs and credential onboarding
Figure 254. Provider Settings explains each credential and links to the provider console

42.2. Bind the client

Create one PushClient in init(), retain it in a field on the main application class, and call register() from start() after showing any permission rationale required by your UX:

private PushClient push;

public void init(Object context) {
    push = PushClient.builder("cn1_push_application_key")
            .listener(new PushListener() {
                @Override
                public void onRegistration(PushSubscription subscription) {
                    Log.p("Push ready on " + subscription.getTransportId());
                }

                @Override
                public void onMessage(PushMessage message) {
                    if (message.getDeepLink() != null) {
                        Log.p("Route to " + message.getDeepLink());
                    }
                }

                @Override
                public void onError(PushError error) {
                    Log.p(error.getCode() + ": " + error.getMessage());
                }
            })
            .build();
}

public void start() {
    push.register();
}

public void stop() {
    // Keep the subscription and listener active while the app is paused.
}

public void disableNotifications() {
    // Use only for an explicit user opt-out.
    push.unregister();
}

start() can run more than once during one process lifetime. register() is idempotent, so the example can call it on every start without creating duplicate native registrations. Don’t call unregister() from stop(): it removes the device subscription. Call it only for an explicit user opt-out or account-removal workflow.

Registration is asynchronous. With managed delivery, the framework registers the native token with BuildCloud automatically. PushSubscription exposes the transport identifier, opaque native token, platform, installation ID, expiry, and capability list for diagnostics. Application code shouldn’t parse the token or use it as a user identity.

On Android, the native bootstrap can obtain and persist an FCM or Huawei token before start() installs the PushClient callback. register() replays that persisted token after activating the listener and then asks the provider to refresh it, so the documented init()/start() lifecycle doesn’t lose early registration.

42.2.1. Listener lifecycle and early messages

PushClient.Builder.listener() is mandatory, and build() fails immediately if it’s missing. The client retains that listener until deregistration completes. Codename One doesn’t locate listeners with reflection or Class.forName(): the application must build, retain, and register the client explicitly. Only one PushClient can be active in a process; a second client reports an active_client error instead of replacing the first listener.

Every PushListener and PushRegistrationSink callback runs on the Codename One EDT:

  • onRegistration() runs after initial native registration and can run again when a provider rotates its token. Replace the previous server-side token for that installation.

  • onMessage() receives one parsed schema-3 envelope. A foreground notification can arrive immediately. A visible background notification normally arrives after the user opens it.

  • onError() reports registration and envelope errors. Use PushError.getCode() for decisions; diagnostic text isn’t a stable contract.

Native ports preserve a cold-start notification until the runtime can deliver it. If the runtime starts before the application calls register(), PushClient keeps a bounded process-local queue and replays it after activation. If the application never registers its client, it has provided no listener and can’t expect application callbacks. Silent/background execution remains subject to each OS’s power and scheduling rules, so refresh authoritative state whenever the application resumes.

42.3. Build a message

PushMessage is the canonical message model on both sides of the wire:

PushMessage message = PushMessage.builder()
        .title("Order shipped")
        .body("Order 4815 is on its way")
        .deepLink("myapp://orders/4815")
        .imageUrl("https://example.com/orders/4815.png")
        .collapseKey("order-4815")
        .ttlSeconds(3600)
        .data("orderId", "4815")
        .build();

Common fields have consistent meanings on every provider. Add provider-specific settings under the platform object only when the common model isn’t sufficient. Unknown settings are ignored by unrelated providers.

{
  "schema": 3,
  "title": "Order shipped",
  "body": "Order 4815 is on its way",
  "deepLink": "myapp://orders/4815",
  "collapseKey": "order-4815",
  "ttl": 3600,
  "data": { "orderId": "4815" },
  "platform": {
    "apns": { "badge": 3, "sound": "default", "interruptionLevel": "time-sensitive" },
    "fcm": { "channelId": "orders", "priority": "high" },
    "huawei": { "channelId": "orders", "importance": "HIGH" },
    "wns": { "type": "toast" },
    "web": { "urgency": "high" }
  }
}

Don’t place secrets or irreplaceable data in a notification. Providers, operating systems, and lock-screen UI may retain or expose payloads.

42.4. Send from a server

Open Account > Server API keys, enter a key name, select the push scope, and select Create server key. Copy the key when it’s shown; it won’t be displayed again. Keep it on your server. Authenticate with Authorization: Bearer YOUR_SERVER_API_KEY or X-API-Key: YOUR_SERVER_API_KEY; the existing classic push token won’t authenticate V3 requests.

Copy the selected application’s UUID from Push > Overview > Server API > Copy ID. Put that UUID in appId, then submit a typed message and one or more provider targets to POST /api/v3/push/messages:

curl -X POST https://cloud.codenameone.com/api/v3/push/messages \
  -H 'Authorization: Bearer YOUR_SERVER_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "appId": "APPLICATION_ID",
    "targets": [
      {"provider":"fcm", "token":"OPAQUE_DEVICE_TOKEN"},
      {"provider":"apns", "token":"OPAQUE_DEVICE_TOKEN"}
    ],
    "message": {
      "schema":3,
      "title":"Order shipped",
      "body":"Order 4815 is on its way",
      "data":{"orderId":"4815"}
    }
  }'

A successful admission returns HTTP 202 with durable delivery IDs. Invalid targets are deactivated when the provider identifies them as permanent failures.

The complete request, response, authentication, error, target, and message schemas are in the Push sending OpenAPI 3.0 specification. Import that file into Swagger UI, an OpenAPI code generator, or an API client instead of reproducing DTOs from this chapter.

Use the Console for operator-driven messages, stored audiences, campaigns, analytics, and automations.

42.5. Migrate an existing push server

There are two migration paths. Choose one wire format for each request:

PathEndpoint and requestAuthentication and installed apps

Keep the classic API

https://cloud.codenameone.com/push/push, with the existing form-encoded fields

Keep the classic token, provider credentials, and installed PushCallback apps.

Move the server to V3

https://cloud.codenameone.com/api/v3/push/messages, with a JSON body

Use a server API key with push scope, the application UUID, saved provider credentials, and message.classic for installed PushCallback apps.

For the classic path, change only the host of the existing /push/push URL. Keep fields such as token, device, type, body, auth, cert, certPassword, and production. Don’t post these form fields to /api/v3/push/messages. A Java URL string must contain the plain URL, without Markdown brackets or parentheses.

For a server-only V3 migration, configure credentials for the same Firebase project and Apple bundle ID/environment as the installed apps. Keep existing device keys and select the explicit classic payload:

{
  "appId": "YOUR_APPLICATION_UUID",
  "targets": [
    {"provider": "fcm", "token": "cn1-fcm-YOUR_ANDROID_DEVICE_TOKEN"},
    {"provider": "apns", "token": "cn1-ios-YOUR_IOS_DEVICE_TOKEN"}
  ],
  "message": {
    "schema": 3,
    "classic": {"type": 1, "body": "Hello"}
  }
}

Send this JSON with the same headers as the V3 curl example above. Replace each target with a real device key for that provider. Existing cn1-fcm-, cn1-gcm-, and cn1-ios- prefixes are accepted. Keep the numeric push type and body encoding used by your existing sender; the example uses type 1 for a simple message.

message.classic produces the fields consumed by installed PushCallback clients. It doesn’t require platform.fcm/platform.apns overrides, a PushClient integration, or a new mobile release. Don’t combine classic with the ordinary V3 message fields. Use ordinary V3 messages for clients that have adopted PushClient.

Before changing production traffic, send to installed Android and iOS test apps. Verify foreground delivery, background delivery, notification taps, and any other push types your application uses.

42.5.1. Interpret the response

HTTP 202 means the request was admitted to the durable queue. Its JSON response contains deliveryIds and quota; it doesn’t mean the provider or device has accepted the message yet. Use delivery feedback and analytics for later outcomes.

  • 401: check the server API key, its push scope, and the authentication header. A classic push token or client binding key isn’t a V3 server key.

  • 400: read the JSON error, check that appId is the selected application UUID, and check the request or credential format. For the classic endpoint, credential URL returned HTTP 404 means a URL supplied in auth or cert couldn’t be fetched. Firebase auth can also contain the complete service-account JSON directly.

  • 406 after a notification arrives: this isn’t a successful API response, and changing the message format or resending can duplicate the notification. Report the UTC request time, endpoint, response body, and CF-RAY header when present, without including credentials or device tokens. The expected successful V3 response remains 202 with delivery IDs.

42.6. Learn which sends failed

A notification is acknowledged when it’s admitted, not when it arrives. Both wire APIs answer before any provider has been asked — the classic endpoint replies queued — so the verdict lands seconds later, after your request has already returned. Nothing in the response can tell you that a device key is dead, and a dead key stays in your database being sent to.

Delivery feedback closes that. Configure an HTTPS endpoint and BuildCloud posts a signed digest of the outcomes since the last one, once a day. It’s available on Pro and above, and it’s configured per organization rather than per application: callers on the classic API register an application per send, so there is nothing stable there to attach a setting to.

Open Push > Settings > Delivery feedback, enter the endpoint, enable the daily digest, and select Send test event. The test posts one synthetic event through the same signing and sending path as a real digest and reports what your endpoint answered, so a broken receiver is visible immediately rather than a day later. The signing secret is on the same card.

42.6.1. What arrives

Failures are itemized and successes are counted. A busy application produces thousands of accepted deliveries a day and nothing can be done about any of them, so listing them would bury the handful of events that matter.

{
  "version": 1,
  "organization": "9f1c2f7a-2f6e-4a5b-9d71-3f0a7c1d5e22",
  "generatedAt": 1790179200000,
  "window": {"from": 1790092800000, "to": 1790179200000},
  "summary": {
    "fcm":  {"ACCEPTED": 7412, "FAILED": 1},
    "apns": {"ACCEPTED": 3310},
    "web":  {"DEAD": 1}
  },
  "events": [
    {
      "deliveryId": "6f0f6f1e-9a1e-4f2a-9a7a-6d5f5f6a1b02",
      "device": "cn1-fcm-fH9c...Qk",
      "token": "fH9c...Qk",
      "provider": "fcm",
      "status": "FAILED",
      "reason": "INVALID_TARGET",
      "attempts": 1,
      "at": 1790178322418,
      "detail": "{\"error\":{\"code\":404,\"status\":\"NOT_FOUND\"}}"
    },
    {
      "deliveryId": "d2b1a0c4-1f77-4a4d-9d0e-2c5b7a91f3aa",
      "device": "cn1-web-https://push.example.com/sub/9?p256dh=KEY&auth=AUTH",
      "endpoint": "https://push.example.com/sub/9",
      "provider": "web",
      "status": "DEAD",
      "reason": "TRANSIENT_FAILURE",
      "attempts": 6,
      "at": 1790178901004,
      "detail": "gave up after repeated 503 responses"
    }
  ],
  "truncated": false,
  "eventsOmitted": 10722
}

reason is the provider-independent verdict, and it decides what you do:

INVALID_TARGET

The provider rejected the key. Delete it from your database; it will never work again.

PERMANENT_FAILURE

The provider refused the message or the credential. Fix the send, keep the key.

TRANSIENT_FAILURE

Retried until the attempt budget ran out. status is DEAD. The key may still be good.

Store the canonical provider and bare token (or web-push endpoint) in separate columns alongside the original key returned by Push.getPushKey(). The full key has a cn1-<provider>- prefix, so comparing it directly with the digest’s bare token matches no row. Normalize gcm to fcm and ios to apns during registration and backfill existing rows before enabling cleanup. For web push, store the subscription endpoint in its own column. Match feedback on the provider and this normalized target, scoped to the organization. Keep the full original key for sending.

42.6.2. Semantics to build against

  • A digest is at-least-once. Deduplicate on deliveryId when present. For classic events without it, use a key built from the provider, token or endpoint, and event timestamp at. A refused digest is resent, and a network failure can deliver one whose response never arrived.

  • Only 2xx acknowledges the window. It advances the watermark and that window is never sent again, so answer 2xx after your writes are durable, not before. Anything else, including a timeout, keeps the window for the next attempt.

  • truncated means more is waiting. A digest is capped, and the next page follows in the same run rather than a day later. Expect several digests in quick succession after a bad day.

  • A failing endpoint is retried, then disabled. Failures are retried on a shorter cadence than the daily digest; after several consecutive failures the endpoint is disabled and the reason appears on the Settings card. Re-enabling it clears the count.

  • eventsOmitted counts accepted deliveries, which appear only in summary.

  • One digest can span applications. The endpoint belongs to the organization, so a receiver that keeps a device store per application correlates on deliveryId — the id the v3 send returned — or on the device key itself, which providers issue per application. A classic send returns no delivery id, so for those the key is the correlation.

42.6.3. Verify the signature

Every request carries X-CN1-Signature: t=<unix-millis>,v1=<hex>, where the hex is HMAC-SHA256 over <t>.<raw body> keyed with your signing secret. The timestamp is inside the signed material, so a captured digest can be rejected by age as well as shown to come from BuildCloud.

Three things are easy to get wrong, and each one fails without a symptom or fails open:

  1. Sign the bytes you received. Any middleware that parses JSON and re-serializes it produces a different byte string that never verifies.

  2. Compare in constant time. A comparison that returns on the first differing byte leaks enough timing to recover a valid signature.

  3. Reject old timestamps. Without an age bound the signature alone permits replay forever.

42.6.4. A Codename One backend receiver

The Chapter 32, Server-side backend takes the raw request when a handler needs the exact body, which is what signature verification requires:

@RestController
@RequestMapping("/push")
public class PushFeedback {

    /**
     * Your organization-scoped durable store. Store provider and bare token (or
     * web endpoint) alongside the original push key; backfill existing rows before
     * enabling cleanup. Both operations belong in ONE transaction: a marker
     * written before the deletion commits turns a retry into a silent skip,
     * and a deletion without a marker is applied twice.
     */
    public interface DeviceStore {
        boolean alreadyApplied(String eventKey);

        // provider + target match normalized registration columns, NOT Push.getPushKey().
        // Enforce a unique eventKey and delete in the same transaction.
        void removeTargetAndMarkApplied(String eventKey, String provider, String target)
                throws Exception;
    }

    private final DeviceStore store;

    /** The signing secret shown in Push > Settings. */
    private final String secret;

    /**
     * Both are injected: the store is your bean implementing DeviceStore, and
     * the secret is read from cn1.push.callbackSecret, which the environment
     * variable CN1_PUSH_CALLBACKSECRET overrides.
     */
    public PushFeedback(DeviceStore store,
                        @Value("${cn1.push.callbackSecret}") String secret) {
        this.store = store;
        this.secret = secret;
    }

    /** Reject a digest whose timestamp is older than this, to bound replay. */
    private static final long MAX_AGE_MS = 5 * 60 * 1000L;

    @PostMapping("/feedback")
    public HttpServer.Response feedback(HttpServer.Request request) throws Exception {
        String body = request.getBody();
        if (!verified(request.getHeader("X-CN1-Signature"), body, secret)) {
            // Anything but 2xx keeps the window at the sender and resends it,
            // which is what you want while a secret rotation is half-applied.
            return new HttpServer.Response(401, "text/plain",
                    "bad signature".getBytes(StandardCharsets.UTF_8));
        }
        Map digest = Json.parseObject(body);
        List events = (List) digest.get("events");
        if (events != null) {
            for (Object entry : events) {
                Map event = (Map) entry;
                if (!"INVALID_TARGET".equals(event.get("reason"))) {
                    continue;
                }
                String provider = (String) event.get("provider");
                String target = (String) event.get("token");
                if (target == null || target.length() == 0) {
                    target = (String) event.get("endpoint");
                }
                if (provider == null || provider.length() == 0
                        || target == null || target.length() == 0) {
                    throw new IllegalArgumentException("Missing push target");
                }
                String deliveryId = (String) event.get("deliveryId");
                String eventKey;
                if (deliveryId != null && deliveryId.length() > 0) {
                    eventKey = "delivery:" + deliveryId;
                } else {
                    Object at = event.get("at");
                    if (at == null) {
                        throw new IllegalArgumentException("Missing classic event timestamp");
                    }
                    eventKey = "classic:" + provider + ":" + target.length()
                            + ":" + target + ":" + at;
                }
                if (!store.alreadyApplied(eventKey)) {
                    store.removeTargetAndMarkApplied(eventKey, provider, target);
                }
            }
        }
        // 2xx acknowledges that this window is durably applied: it advances the
        // sender's watermark and the window is never sent again. Answer it
        // after the writes above have committed, not alongside them.
        return new HttpServer.Response(200, "text/plain",
                "ok".getBytes(StandardCharsets.UTF_8));
    }

    // throws, because String.getBytes(Charset) is a CHECKED throw in the
    // ParparVM class library even though it is not one on a JVM.
    private static boolean verified(String header, String body, String secret)
            throws Exception {
        if (header == null || secret == null || secret.length() == 0) {
            return false;
        }
        long timestamp = 0;
        String provided = null;
        // Parsed by hand: java.lang.String has no split() in the ParparVM class
        // library, so the obvious version of this compiles for the development
        // run and fails the native package step.
        int cursor = 0;
        while (cursor < header.length()) {
            int comma = header.indexOf(',', cursor);
            if (comma < 0) {
                comma = header.length();
            }
            String part = header.substring(cursor, comma);
            cursor = comma + 1;
            int equals = part.indexOf('=');
            if (equals < 1) {
                continue;
            }
            String name = part.substring(0, equals).trim();
            String value = part.substring(equals + 1).trim();
            if ("t".equals(name)) {
                timestamp = Long.parseLong(value);
            } else if ("v1".equals(name)) {
                provided = value;
            }
        }
        if (provided == null
                || Math.abs(System.currentTimeMillis() - timestamp) > MAX_AGE_MS) {
            return false;
        }
        // Crypto, not javax.crypto: this class is recompiled against the
        // ParparVM class library by cn1:backend-package, and that library has
        // no JCE. equalsConstantTime is here for the same reason a hand-written
        // loop would be -- an early exit on the first differing byte lets a MAC
        // be forged one byte at a time.
        byte[] expected = Crypto.hmacSha256(secret.getBytes(StandardCharsets.UTF_8),
                (timestamp + "." + body).getBytes(StandardCharsets.UTF_8));
        return Crypto.equalsConstantTime(expected, decodeHex(provided));
    }

    private static byte[] decodeHex(String value) {
        if (value.length() % 2 != 0) {
            return new byte[0];
        }
        byte[] out = new byte[value.length() / 2];
        for (int i = 0; i < out.length; i++) {
            int high = Character.digit(value.charAt(i * 2), 16);
            int low = Character.digit(value.charAt(i * 2 + 1), 16);
            if (high < 0 || low < 0) {
                return new byte[0];
            }
            out[i] = (byte) ((high << 4) | low);
        }
        return out;
    }
}

42.6.5. A long-running Node server

The same three rules, with the raw-body trap in the form it usually takes:

// Express. Verify before any JSON middleware touches the body: the signature
// covers the bytes we sent, and express.json() hands you an object whose
// re-serialisation is not byte-identical, so it never verifies.
const express = require('express');
const crypto = require('crypto');

const app = express();
const SECRET = process.env.CN1_PUSH_CALLBACK_SECRET;
const MAX_AGE_MS = 5 * 60 * 1000;

function verifySignature(header, body) {
  const parts = Object.fromEntries((header || '').split(',')
      .map(p => [p.slice(0, p.indexOf('=')).trim(), p.slice(p.indexOf('=') + 1).trim()]));
  const expected = Buffer.from(
      crypto.createHmac('sha256', SECRET).update(`${parts.t}.${body}`).digest('hex'), 'utf8');
  const provided = Buffer.from(parts.v1 || '', 'utf8');
  return expected.length === provided.length
      && crypto.timingSafeEqual(expected, provided)
      && Math.abs(Date.now() - Number(parts.t)) <= MAX_AGE_MS;
}

// Store provider + bare token/web endpoint alongside the original push key.
// Backfill existing registrations before enabling cleanup. The durable store
// is scoped to this endpoint's organization.
function cleanupTarget(event) {
  const provider = event.provider;
  const target = event.token || event.endpoint;
  if (typeof provider !== 'string' || !provider
      || typeof target !== 'string' || !target) {
    throw new Error('Missing push target');
  }
  let eventKey;
  if (typeof event.deliveryId === 'string' && event.deliveryId) {
    eventKey = `delivery:${event.deliveryId}`;
  } else {
    if (!Number.isSafeInteger(event.at)) {
      throw new Error('Missing classic event timestamp');
    }
    eventKey = `classic:${provider}:${target.length}:${target}:${event.at}`;
  }
  return {eventKey, provider, target};
}

app.post('/push/feedback', express.raw({type: 'application/json'}), async (req, res) => {
  const body = req.body.toString('utf8');
  if (!verifySignature(req.get('X-CN1-Signature'), body)) {
    return res.sendStatus(401);       // not 2xx, so the same window is resent
  }

  try {
    for (const event of JSON.parse(body).events || []) {
      // Digests are at-least-once, so the deduplication marker and the
      // deletion have to commit together: a marker stored first turns a retry
      // into a silent skip, and a deletion without one is applied twice.
      if (event.reason === 'INVALID_TARGET') {
        const {eventKey, provider, target} = cleanupTarget(event);
        // Enforce eventKey uniqueness and commit marker + removal together.
        await removeTargetIfNotApplied(eventKey, provider, target);
      }
    }
  } catch (failure) {
    // Let it be resent rather than acknowledging work that did not land.
    return res.sendStatus(500);
  }

  // 2xx only now: it advances our watermark and this window is never sent again.
  res.sendStatus(200);
});

42.6.6. A serverless function

A function behind an HTTP gateway differs in two ways that are silent when they are wrong: the gateway may hand you the body base64-encoded, and there is no process to keep deduplication state in.

// A serverless function behind an HTTP gateway. Two things differ from a
// long-lived server, and both are silent when they are wrong.
const crypto = require('crypto');

const SECRET = process.env.CN1_PUSH_CALLBACK_SECRET;
const MAX_AGE_MS = 5 * 60 * 1000;

function verifySignature(header, body) {
  const parts = Object.fromEntries((header || '').split(',')
      .map(p => [p.slice(0, p.indexOf('=')).trim(), p.slice(p.indexOf('=') + 1).trim()]));
  const expected = Buffer.from(
      crypto.createHmac('sha256', SECRET).update(`${parts.t}.${body}`).digest('hex'), 'utf8');
  const provided = Buffer.from(parts.v1 || '', 'utf8');
  return expected.length === provided.length
      && crypto.timingSafeEqual(expected, provided)
      && Math.abs(Date.now() - Number(parts.t)) <= MAX_AGE_MS;
}

exports.handler = async (event) => {
  // 1. The gateway may hand you the body base64-encoded, and it decides that
  //    by sniffing content rather than by anything you control. Hashing the
  //    encoded form verifies nothing and fails closed on every digest.
  const body = event.isBase64Encoded
      ? Buffer.from(event.body, 'base64').toString('utf8')
      : event.body;

  // Header names arrive lower-cased from some gateways and not from others.
  const headers = event.headers || {};
  const signature = headers['x-cn1-signature'] || headers['X-CN1-Signature'];
  if (!verifySignature(signature, body)) {
    return {statusCode: 401, body: 'bad signature'};
  }

  // 2. There is no process to keep state in, so deduplication and the device
  //    table both have to be the database. A warm container that remembers
  //    delivery ids is an optimisation, never the correctness mechanism.
  // applyDurably must use the same provider + target normalization and
  // deliveryId/classic-event fallback as the long-running receiver above.
  await applyDurably(JSON.parse(body).events || []);

  // Return after the writes, not alongside them: a function that answers 200
  // and is frozen before its writes flush has told us to advance the watermark
  // over a window nobody stored.
  return {statusCode: 200, body: 'ok'};
};

42.6.7. A Spring server

byte[] rather than a mapped type, for the reason above: what Jackson parses and re-serializes is a different byte string.

@RestController
public class PushFeedbackSpring {

    private static final long MAX_AGE_MS = 5 * 60 * 1000L;

    // An injected collaborator, NOT a method on this class. Spring's
    // transaction management is proxy-based, so a call from one method of this
    // bean to another of its own never passes through the interceptor, and a
    // @Transactional there would do nothing at all -- without saying so.
    private final PushFeedbackApplier applier;
    private final byte[] secret;

    public PushFeedbackSpring(PushFeedbackApplier applier,
            @Value("${cn1.push.callback.secret}") String secret) {
        this.applier = applier;
        this.secret = secret.getBytes(StandardCharsets.UTF_8);
    }

    // byte[], not a mapped type: the signature covers the bytes as sent, and
    // anything Jackson parses and re-serialises is a different byte string that
    // never verifies.
    @PostMapping(path = "/push/feedback", consumes = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<String> feedback(
            @RequestHeader(name = "X-CN1-Signature", required = false) String signature,
            @RequestBody byte[] raw) throws Exception {
        String body = new String(raw, StandardCharsets.UTF_8);
        if (!verified(signature, body, secret)) {
            // Anything but 2xx keeps the window at the sender and resends it.
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("bad signature");
        }
        JsonNode digest = new ObjectMapper().readTree(body);
        for (JsonNode event : digest.path("events")) {
            applier.apply(event);
        }
        // 2xx advances the sender's watermark, so it is returned only after
        // every event above has committed.
        return ResponseEntity.ok("ok");
    }

    static boolean verified(String header, String body, byte[] secret) throws Exception {
        if (header == null) {
            return false;
        }
        long timestamp = 0;
        String provided = null;
        for (String part : header.split(",")) {
            int equals = part.indexOf('=');
            if (equals < 1) {
                continue;
            }
            String name = part.substring(0, equals).trim();
            String value = part.substring(equals + 1).trim();
            if ("t".equals(name)) {
                timestamp = Long.parseLong(value);
            } else if ("v1".equals(name)) {
                provided = value;
            }
        }
        if (provided == null
                || Math.abs(System.currentTimeMillis() - timestamp) > MAX_AGE_MS) {
            return false;                      // bound replay by age
        }
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(secret, "HmacSHA256"));
        byte[] expected = mac.doFinal(
                (timestamp + "." + body).getBytes(StandardCharsets.UTF_8));
        // MessageDigest.isEqual is the JDK's constant-time comparison. Never
        // Arrays.equals or String.equals here: both return at the first
        // differing byte, and that timing is enough to forge a signature.
        return MessageDigest.isEqual(expected, HexFormat.of().parseHex(provided));
    }
}

@Component
class PushFeedbackApplier {

    private final DeviceStore store;

    PushFeedbackApplier(DeviceStore store) {
        this.store = store;
    }

    /**
     * One transaction per event: the deduplication marker and the deletion have
     * to commit together. A marker written first turns a retry into a silent
     * skip; a deletion without one is applied twice.
     */
    @Transactional
    public void apply(JsonNode event) {
        if (!"INVALID_TARGET".equals(event.path("reason").asText(null))) {
            return;
        }
        String provider = event.path("provider").asText(null);
        String target = event.path("token").asText(null);
        if (target == null || target.isEmpty()) {
            target = event.path("endpoint").asText(null);
        }
        if (provider == null || provider.isEmpty() || target == null || target.isEmpty()) {
            throw new IllegalArgumentException("Missing push target");
        }
        String deliveryId = event.path("deliveryId").asText(null);
        String eventKey;
        if (deliveryId != null && !deliveryId.isEmpty()) {
            eventKey = "delivery:" + deliveryId;
        } else {
            String at = event.path("at").asText(null);
            if (at == null) {
                throw new IllegalArgumentException("Missing classic event timestamp");
            }
            eventKey = "classic:" + provider + ":" + target.length() + ":" + target + ":" + at;
        }
        if (store.alreadyApplied(eventKey)) {
            return;
        }
        store.removeTarget(provider, target);
        store.markApplied(eventKey);
    }
}

42.6.8. A MicroProfile or Jakarta REST server

The same shape with @HeaderParam and a byte[] entity, taking the secret from MicroProfile Config:

@Path("/push")
@ApplicationScoped
public class PushFeedbackMicroProfile {

    // Injected, not a method on this bean: CDI interceptors are proxy-based
    // exactly like Spring's, so this.apply(...) would never reach the
    // @Transactional interceptor and the guarantee would be silently absent.
    @Inject
    PushFeedbackCdiApplier applier;

    // MicroProfile Config, so the secret arrives the same way as every other
    // deployment value rather than as a constant in the source.
    @Inject
    @ConfigProperty(name = "cn1.push.callback.secret")
    String secret;

    // byte[] entity, for the reason it is byte[] everywhere else: a provider
    // that binds this to a POJO reads the bytes, and the re-serialised form no
    // longer matches the signature.
    @POST
    @Path("/feedback")
    @Consumes(MediaType.APPLICATION_JSON)
    public Response feedback(@HeaderParam("X-CN1-Signature") String signature, byte[] raw)
            throws Exception {
        String body = new String(raw, StandardCharsets.UTF_8);
        // The verification is protocol, not framework: HMAC-SHA256 over
        // "<t>.<raw body>", constant-time compare, timestamp inside the window.
        if (!PushFeedbackSpring.verified(signature, body,
                secret.getBytes(StandardCharsets.UTF_8))) {
            return Response.status(Response.Status.UNAUTHORIZED).entity("bad signature").build();
        }
        try (JsonReader reader = Json.createReader(new StringReader(body))) {
            JsonObject digest = reader.readObject();
            JsonValue events = digest.get("events");
            if (events != null) {
                for (JsonValue value : events.asJsonArray()) {
                    applier.apply(value.asJsonObject());
                }
            }
        }
        return Response.ok("ok").build();
    }
}

@ApplicationScoped
class PushFeedbackCdiApplier {

    @Inject
    DeviceStore store;

    /** Marker and deletion in one transaction, as in the Spring receiver. */
    @Transactional
    public void apply(JsonObject event) {
        if (!"INVALID_TARGET".equals(event.getString("reason", null))) {
            return;
        }
        String provider = event.getString("provider", null);
        String target = event.getString("token", null);
        if (target == null || target.isEmpty()) {
            target = event.getString("endpoint", null);
        }
        if (provider == null || provider.isEmpty() || target == null || target.isEmpty()) {
            throw new IllegalArgumentException("Missing push target");
        }
        String deliveryId = event.getString("deliveryId", null);
        String eventKey;
        if (deliveryId != null && !deliveryId.isEmpty()) {
            eventKey = "delivery:" + deliveryId;
        } else {
            String at = (event.get("at") == null ? null : event.get("at").toString());
            if (at == null) {
                throw new IllegalArgumentException("Missing classic event timestamp");
            }
            eventKey = "classic:" + provider + ":" + target.length() + ":" + target + ":" + at;
        }
        if (store.alreadyApplied(eventKey)) {
            return;
        }
        store.removeTarget(provider, target);
        store.markApplied(eventKey);
    }
}

42.6.9. Any other stack

The contract is a signed JSON POST, so a receiver in Go, Python, PHP, Ruby or a JVM framework is the same three steps: HMAC-SHA256 over <t>.<raw body>, constant-time compare against v1, reject a t outside your age window. Then apply each event no more than once, and answer 2xx after it’s stored.

42.7. Build audiences in the Console

Managed audiences are application-scoped. They don’t contain copied device tokens. A saved segment stores a dynamic filter, and BuildCloud resolves that filter against active subscriptions when a campaign launches.

Your authenticated server assigns an external user ID and tags to an installation with PUT /api/v3/push/apps/{appId}/installations/{installationId}. Tags should be stable application facts such as subscription=active, region=emea, role=technician, or testUser=true. Don’t let an untrusted client grant itself a privileged tag.

{
  "externalUserId": "customer-4815",
  "tags": {
    "subscription": "active",
    "region": "emea",
    "testUser": "false"
  }
}

Open Push > Audience. The tag and value pickers contain values discovered from current active subscriptions for the selected application. Choose an optional platform, choose one tag rule, enter a segment name, and select Save segment. The new segment appears immediately under Saved segments with its current recipient estimate. It also becomes available in the Messages recipient picker.

Push Audience builder and saved segment list
Figure 255. Audience uses discovered tag values and shows every saved segment

Segments are evaluated at send time. Changing a subscription’s tags changes future membership without editing the segment. A count is an estimate of the current state; registrations, subscription removals, and tag updates can change it before admission.

For a safe rollout, tag internal devices with testUser=true, create a test-user segment, and send the message there first. Create the production segment only from server-controlled tags.

42.8. Send a message from the Console

Open Push > Messages and complete the three numbered sections:

  1. Audience: choose All subscribed devices or a saved segment. The Console displays the current recipient estimate. Individual provider tokens aren’t entered in this workflow.

  2. Message: enter a notification title and message. A visible notification requires message text. For a silent notification, enable Silent / data-only notification and provide an additional-data key and value. The Console builds valid JSON; operators don’t paste JSON into the primary composer.

  3. Delivery: choose how long providers may retain an undelivered message. Console messages use safe cross-platform defaults and enter the durable queue immediately.

New Push Message composer with audience, message, and delivery sections
Figure 256. The restrictive message composer accepts a saved audience and typed fields

Select Queue message only after checking the audience name and estimate. BuildCloud creates the campaign, resolves the segment, and admits the recipients to the durable delivery queue. The provider may accept a notification that the device never displays, so inspect Push > Analytics for provider acceptance and failures without treating acceptance as device delivery.

42.9. Event automations

An automation is an enabled rule containing trigger.event, a schema-3 message, an optional audience, and an optional delaySeconds. If no audience is stored in the rule, the event must identify an externalUserId or installationId. Text fields in the message can insert event properties with ${event.propertyName}.

{
  "enabled": true,
  "trigger": {"event": "order.ready"},
  "delaySeconds": 0,
  "message": {
    "schema": 3,
    "title": "Order update",
    "body": "Order ${event.orderId} is ready"
  }
}

Your server triggers enabled rules with POST /api/v3/push/apps/{appId}/events using a push-scoped API key:

{
  "name": "order.ready",
  "externalUserId": "customer-4815",
  "properties": {"orderId": "4815"}
}

Immediate matches enter the durable delivery queue. Delayed matches become scheduled campaigns, so restarts don’t lose them. Automations are intentionally push rules, not a general-purpose customer journey or analytics product.

42.10. Widgets and live surfaces

A push can publish widget content or update a live activity before the regular listener receives the message. The surface object uses one of these operations:

{
  "schema": 3,
  "silent": true,
  "surface": {
    "operation": "widget",
    "kind": "order-status",
    "timeline": "{...serialized widget timeline...}"
  }
}

For a live activity use live-update or live-end with id, serialized state, and optional dismissImmediately. Unsupported surfaces are ignored. A surface command runs when the Codename One runtime receives the envelope. On platforms that don’t start application code for a background push, the native queue preserves the command and applies it on the next start or resume. Applications must therefore refresh stale surface state during foreground activation too.

42.11. Use an application-owned push server

A custom server is a low-level alternative to managed push; it doesn’t reuse the BuildCloud protocol at a different endpoint. A CN1Lib implements PushTransport around the private provider’s native SDK. It obtains native registration material, reports token rotation, and forwards incoming data as the canonical schema-3 JSON envelope. A matching PushRegistrationSink stores and removes the subscription on your server:

// companyTransport is supplied by the native CN1Lib.
// listener is the application's normal, non-null PushListener.
PushClient client = PushClient.builder("private-app-id")
        .transport(companyTransport)
        .registrationSink(new PushRegistrationSink() {
            @Override
            public void registered(PushSubscription value) {
                Log.p("Send the subscription to the company server");
            }

            @Override
            public void unregistered(PushSubscription value) {
                Log.p("Remove the subscription from the company server");
            }
        })
        .listener(listener)
        .build();

When a PushTransport implementation is present, PushClient never contacts BuildCloud. Your implementation owns these responsibilities:

  • getId() returns a stable provider ID that your server understands.

  • isSupported() checks whether the SDK can operate on this device.

  • register() invokes Callback.registered() with a PushSubscription, then reports later token rotations through the same callback.

  • unregister() removes the provider subscription and invokes Callback.unregistered() when complete.

  • Incoming native messages call Callback.message() with one complete schema-3 envelope. Don’t call the application listener directly and don’t invent a second payload format.

  • Registration or transport failures call Callback.failed(). The callback is thread-safe; PushClient moves application-facing callbacks to the EDT.

The registration sink should asynchronously insert or update the subscription by installationId on your server and replace its token when it rotates. The server then sends through your provider’s native API. It must preserve the schema-3 envelope so the same PushListener, deep-link handling, and surface commands work in managed and custom-server modes.

The application still follows the normal lifecycle: retain one client, call register() from start(), and reserve unregister() for an actual opt-out. This custom integration doesn’t introduce reflection or automatic listener discovery.

42.12. Testing delivery behavior

Hosted CI has no installed application, OS push daemon, permission state, or physical device, so it can’t prove provider-to-device delivery. Codename One tests at the lowest controllable boundary instead. PushClientTransportTest injects a fake native transport and covers:

  • foreground, background, force-stopped, and cold-start delivery;

  • notification tap, action, dismissal, and deep-link routing;

  • permission granted, denied, and later changed in system settings;

  • token rotation, reinstall, expiry, logout, and multiple installations per user;

  • silent delivery and simulated constrained-state replay;

  • collapse keys, TTL expiry, Unicode, maximum payloads, images, and provider errors;

  • queued widget and live-activity updates replayed at the native/runtime seam;

  • retry idempotency and dead-letter behavior.

The JavaScript contract executes the real service-worker source with mocked browser clients and notification APIs. BuildCloud has a separate local suite using mocked provider HTTP responses for compatibility translation, credentials, durable queue behavior, retries, and automations. Physical-device sends are optional release diagnostics rather than a CI or nightly requirement. Provider acceptance isn’t proof of device delivery.

43. Notifications and Background Execution

Codename One exposes a cross-platform API for rich local notifications, runtime notification permission, notification channels, constraint-aware background work, foreground services, deferrable background processing, receiving content shared from other apps, and push topic subscriptions. Each capability degrades on platforms that can’t support it — calls become documented no-ops rather than errors — and the JavaSE simulator implements them so you can exercise the flows without a device.

Because these features build on platform-specific behavior, every section below lists the support per platform. In the matrices: Yes = fully supported, Partial = supported with documented limitations, No-op = the call is accepted but does nothing, Sim = exercised in the simulator.

43.1. Notification permission

On iOS, and on Android 13+ (POST_NOTIFICATIONS), an app must obtain permission before its notifications are shown. Request it through Display:

NotificationPermissionRequest req = new NotificationPermissionRequest()
        .provisional(true)      // iOS: deliver quietly without an explicit prompt
        .timeSensitive(true);   // iOS: allow the time-sensitive interruption level

Display.getInstance().requestNotificationPermission(req, result -> {
    if (result.isGranted()) {
        // schedule notifications
    }
    AuthorizationLevel level = result.getAuthorizationLevel();
});

NotificationPermissionResult.getAuthorizationLevel() returns an AuthorizationLevel enum (NOT_DETERMINED, DENIED, AUTHORIZED, PROVISIONAL, EPHEMERAL). The values are ordered from least to most permissive, so isGranted() is "more permissive than DENIED." The callback is delivered on the EDT.

Request optioniOSAndroidSimulator

alert / sound / badge

Yes

Implied by POST_NOTIFICATIONS

Granted

provisional

Yes (iOS 12+)

No-op

Granted

timeSensitive

Yes (iOS 15+, needs entitlement)

No-op

Granted

critical

Yes (needs Apple entitlement)

No-op

Granted

carPlay / announcement

Yes

No-op

Granted

The timeSensitive and critical options require an Apple capability on the App ID. Enable injection of the matching entitlement with the ios.timeSensitiveNotifications=true and ios.criticalAlerts=true build hints (they’re opt-in because an entitlement the provisioning profile doesn’t carry breaks code signing).

43.2. Local notifications

LocalNotification is scheduled through Display.scheduleLocalNotification(…​) as before; the bean has been extended with fluent, backward-compatible setters:

LocalNotification n = new LocalNotification()
        .setChannelId("messages")                 // Android channel (see below)
        .setGroup("chat-42")                       // bundle related notifications
        .setProgress(100, 40)                      // determinate progress bar (Android)
        .setTimeSensitive(true);                   // break through Focus / elevated importance
n.setId("msg-42");
n.setAlertTitle("New message");
n.setAlertBody("Tap to reply");
n.addAction("open", "Open");
n.addInputAction("reply", "Reply", "Type a message", "Send");  // inline quick reply
Display.getInstance().scheduleLocalNotification(n, System.currentTimeMillis() + 1000,
        LocalNotification.REPEAT_NONE);

When the user taps an action, the selected action id (and any quick-reply text) are delivered through com.codename1.push.PushContent — getActionId(), getActionTitle() and getTextResponse() — before your LocalNotificationCallback.localNotificationReceived(id) runs, so push and local notification actions are handled identically.

CapabilityiOSAndroidSimulator

title / body / badge / sound

Yes

Yes

Sim (panel)

image attachment

Yes (UNNotificationAttachment)

Yes (BigPicture)

Sim (thumbnail)

action buttons

Yes (UNNotificationAction)

Yes

Sim (buttons)

quick-reply input action

Yes (UNTextInputNotificationAction)

Yes (RemoteInput)

Sim (text field)

grouping

Yes (thread identifier)

Yes (group + summary)

Partial

progress (ongoing)

No-op

Yes

Sim (progress bar)

full-screen intent

No-op

Yes (needs permission, see hints)

No-op

time-sensitive

Yes (iOS 15+)

Approximated (category + priority)

Sim

messaging / conversation style

Partial

Yes (MessagingStyle)

Sim

custom view

Notification content extension

RemoteViews

Sim (render)

43.2.1. Resource names for sound, image and action icons

These string parameters are platform resource names, not numeric ids (Codename One has no R.drawable constants). They’re resolved at runtime per platform:

ParameterWhat to provideAndroid resolutioniOS resolution

alertSound / channel sound

a file whose name starts with notification_sound, bundled in the app

res/raw/<name> via android.resource:// URI

UNNotificationSound from the app bundle

alertImage

image path placed in the app root

loaded from assets/

UNNotificationAttachment from the bundle

Action icon

a drawable name bundled in android/src/main/resources

getResources().getIdentifier(name, "drawable", pkg)

not displayed

The file extension is optional for the action icon and is ignored. iOS notification action buttons don’t render icons, so the icon value is ignored there.

43.3. Notification channels (Android O+)

Channels let the user control importance, sound, vibration, lights, lock screen visibility and badge display for a group of notifications. Build and register one at startup, then reference it from a notification with setChannelId(…​):

new NotificationChannelBuilder("messages", "Messages")
        .description("Incoming chat messages")
        .importance(NotificationChannelBuilder.IMPORTANCE_HIGH)
        .sound("/notification_sound_ping.mp3")
        .enableVibration(true)
        .lightColor(0xff0000)
        .register();

Channels are an Android concept. On iOS and the simulator register() is a no-op (the simulator stores the channel so its name can be shown in the notification panel); the channel id you assign is still carried so the notification behaves consistently.

43.4. Constraint-aware background work

BackgroundWork schedules deferrable work that the OS runs when its constraints are met. Implement BackgroundWorker in a class with a public no-argument constructor. You schedule the class, never an instance: the request records its name, and the scheduler constructs a fresh worker for every run, even when the process was never killed. Its fields are always at their defaults, so pass state through the input data:

/// The platform may rebuild this after the app process was killed, so it
/// needs a public no-argument constructor and can rely on nothing from the
/// foreground app -- state arrives through the input data.
public static class SyncWorker implements BackgroundWorker {
    public void performWork(String id, java.util.Map<String, String> inputData,
            long deadline, com.codename1.util.Callback<Boolean> onComplete) {
        // the real work goes here; inputData carries whatever the request
        // was scheduled with, because no foreground state survives
        String account = inputData.get("account");
        boolean ok = account != null;
        // TRUE for done. FALSE asks for a retry on Android; the iOS port
        // ignores the value, so do not rely on it to recover from failure
        onComplete.onSucess(Boolean.valueOf(ok));
    }
}

void scheduleSync() {
    // on iOS the id must appear in BGTaskSchedulerPermittedIdentifiers, and
    // it gets there only if you ask: ios.usesBackgroundProcessing=true
    // declares <packageName>.processing, which is what this derives
    String taskId = Display.getInstance().getProperty("package_name", "") + ".processing";
    WorkRequest req = WorkRequest.builder(taskId, SyncWorker.class)
            .setRequiresNetwork(true)
            .setRequiresCharging(true)
            .setPeriodic(6 * 60 * 60 * 1000L)
            .putInputData("account", "primary")
            .build();
    BackgroundWork.schedule(req);
}

iOS needs the identifier declared at build time before BGTaskScheduler will accept the request, and nothing detects BackgroundWork on the classpath for you. Set ios.usesBackgroundProcessing=true, which declares <packageName>.processing — the id the sample derives — or list your own ids with ios.backgroundProcessingIds. Without one of those the scheduling call is rejected on the device.

The life of a background work request
iOSAndroidSimulator

backend

BGTaskScheduler (iOS 13+)

JobScheduler

Timer

requiresNetwork / unmetered

Yes

Yes

Toggle in Simulate menu

requiresCharging

Yes (requiresExternalPower)

Yes

Toggle

requiresIdle / batteryNotLow

No-op

Yes

Toggle

periodic

Approximated (resubmit)

Yes (>= 15 min)

Yes

The iOS approximation of a periodic request doesn’t carry the request forward. After a run completes, the port resubmits the task with an earliest date one minute out and with both the network and power requirements cleared, so the work can recur far more often than the period you asked for and outside the constraints you set. Treat a periodic request on iOS as "sometime after the app is next backgrounded," and re-check the conditions you care about at the top of the worker. The completion value is Android-only for the same reason: the iOS port resubmits periodic work whichever value you report, and never resubmits one-shot work, so a FALSE there doesn’t buy you a retry.

BackgroundTask.scheduleProcessing(id, earliestBeginDate, requiresNetwork, requiresPower, runnable) is a one-shot variant for occasional maintenance work. On iOS the identifiers must be declared via the ios.backgroundProcessingIds build hint (comma separated, default <packageName>.processing); the build also adds the processing UIBackgroundMode and links BackgroundTasks.framework automatically.

43.5. Foreground service (Android)

ForegroundService runs a long task while a persistent notification is shown. The first argument names a channel, but the Android service currently routes its notification through the app-wide channel — android.NotificationChannel.id, defaulting to cn1-channel — so the id here doesn’t place the notification in a channel of its own today:

ForegroundService svc = ForegroundService.start("downloads", "Downloading", "Please wait",
        null, service -> {
            for (int i = 0; i <= 100; i++) {
                service.updateNotification("Downloading", i + "%");
                // ... do a chunk of work ...
            }
        });
// auto-stops when the task returns, or call svc.stop()
AndroidiOSSimulator

persistent-notification service

Yes

No (best-effort background window + notification)

Sim (status indicator)

When the app references ForegroundService the build registers the service and injects FOREGROUND_SERVICE plus the per-type FOREGROUND_SERVICE_<TYPE> permission (Android 14). Override the type with the android.foregroundServiceType build hint (default dataSync).

43.6. Receiving shared content

Override onReceivedSharedContent(SharedContent) on your Lifecycle subclass to receive text, URLs, files or images shared into your app from other apps:

public class MyApp extends Lifecycle {
    public void onReceivedSharedContent(SharedContent content) {
        for (SharedContent.Item item : content.getItems()) {
            if (item.getType() == SharedContent.TYPE_IMAGE) {
                // your own import; the path is a FileSystemStorage path
                importImage(item.getFilePath());
            }
        }
    }
}
AndroidiOSSimulator

receive shared content

Yes (SEND / SEND_MULTIPLE)

Yes (share extension + App Group)

Sim ("Send shared content")

On Android, declare the accepted MIME types with the android.shareFilter build hint (comma separated, for example text/plain,image/*); the build registers the share-receiver activity. On iOS this reuses the share-extension authoring pipeline; set the ios.shareAppGroup build hint to the App Group the extension writes to. File and image items are copied into Codename One FileSystemStorage so your code is platform neutral.

43.7. Push topics

Push.subscribeToTopic("news") / unsubscribeFromTopic("news") subscribe the device to a fan-out topic.

AndroidiOS

topic subscribe / unsubscribe

Yes (Firebase Cloud Messaging)

No-op (raw APNs has no topics; fan out server-side)

Server-side topic delivery is handled by the Codename One push server.

43.8. Simulator

The JavaSE simulator implements all the above so you can develop without a device. Use the Simulate → Notifications and Background menu to toggle the background constraints (network / charging / idle / battery), run scheduled work immediately, inspect registered channels, and inject shared content. Posted notifications appear in a panel at the top right; tapping an action (or submitting a quick-reply) routes back through your callback exactly as on device.

44. Calendar integration

The com.codename1.calendar package provides scheduling rather than just an "add event" shortcut. It represents calendars, events, task/reminders, recurrence, attendees, alarms, free/busy intervals, attachments, conference links, paging, incremental synchronization, and explicit edit conflicts.

The API is capability based. Don’t branch on the platform name. Ask the source what it can do:

LocalCalendarSource local = LocalCalendarSource.getInstance();
CalendarCapabilities capabilities = local.getCapabilities();

if (capabilities.supports(CalendarCapability.READ_EVENTS)) {
    local.requestAuthorization(CalendarAccess.EVENTS_READ_ONLY).ready(status -> {
        if (status == CalendarAuthorizationStatus.FULL) {
            local.queryEvents(new CalendarQuery()
                    .setCalendarId("primary")
                    .setStartTime(Instant.now()))
                 .ready(page -> page.getItems().forEach(System.out::println));
        }
    });
}

LocalCalendarSource is implemented with Android’s Calendar Provider and Apple EventKit on iOS and macOS. The simulator supplies an isolated, in-memory source so tests never modify the developer’s real calendar. A port that has no local calendar service returns an empty capability set. Online sources and .ics import/export remain available on every port, including JavaScript and Linux.

TargetBuilt-in local sourcePortable alternatives

Android

Calendar Provider events

Google, Microsoft, CalDAV, and .ics

iOS and macOS

EventKit events and reminders

Google, Microsoft, CalDAV, and .ics

Codename One simulator

Isolated in-memory source

Google, Microsoft, CalDAV, and .ics

Win32, native macOS desktop, Linux, and JavaScript

No built-in local source; the capability set is empty

Google, Microsoft, CalDAV, and .ics

This API is a clean replacement for the old CN1Calendar integration. New code shouldn’t mix objects or identifiers from the two APIs.

44.1. Creating an event

All-day values use CalendarDateTime.allDay(LocalDate). Timed values use ZonedDateTime (or the Instant and ZoneId convenience factory). Alarms use Duration, and query ranges and absolute timestamps use Instant. The API uses java.time instead of defining parallel date, time-zone, or duration types. Keeping all-day and timed values separate avoids shifting an all-day event when it crosses a time-zone boundary.

CalendarEvent event = new CalendarEvent()
        .setCalendarId(calendarId)
        .setTitle("Architecture review")
        .setStart(CalendarDateTime.instant(start, ZoneId.of("Europe/Paris")))
        .setEnd(CalendarDateTime.instant(end, ZoneId.of("Europe/Paris")))
        .setRecurrence(new CalendarRecurrenceRule()
                .setFrequency(CalendarRecurrenceRule.Frequency.WEEKLY)
                .addDayOfWeek(2))
        .addAttendee(new CalendarAttendee()
                .setName("Ari")
                .setEmail("[email protected]"))
        .addAlarm(new CalendarAlarm().setTimeBefore(Duration.ofMinutes(15)));

local.saveEvent(event, CalendarMutationScope.ALL).ready(saved -> {
    Log.p("Created " + saved.getId());
});

The version returned by a source is an optimistic-concurrency token. Pass the returned object to saveEvent() or its version to a delete method. A stale token produces CalendarError.CONFLICT instead of overwriting a newer server edit.

44.2. Online sources

Online sources work on all ports and use the application’s OAuth client. They never embed a client secret and never persist credentials implicitly.

OidcClient.discover("https://accounts.google.com").ready(oidc -> {
    oidc.setClientId(clientId)
        .setRedirectUri(redirectUri)
        .setScopes(GoogleCalendarSource.SCOPE_CALENDAR,
                   GoogleCalendarSource.SCOPE_TASKS)
        .authorize().ready(tokens -> {
            CalendarSource google = new GoogleCalendarSource(
                    new OidcCalendarTokenProvider(oidc, tokens));
            CalendarManager.getInstance().registerSource(google);
        });
});

GoogleCalendarSource covers Google Calendar and Google Tasks. MicrosoftCalendarSource covers Microsoft Graph calendars and Microsoft To Do. Both expose provider paging and delta tokens through CalendarPage and retry once with a refreshed token after HTTP 401.

For CalDAV, pass the account’s calendar-home URL and an authentication strategy:

CalendarSource caldav = new CalDavCalendarSource(
        "work", "Work CalDAV", calendarHomeUrl,
        CalDavAuthentication.digest(username, password));

CalDavAuthentication.basic(), bearer(), and digest() are supported. Prefer Bearer or Digest over Basic unless Basic is protected by HTTPS. Calendar discovery filters collections by their advertised VEVENT and VTODO component support. Check the result of each asynchronous operation as servers can restrict individual collections further.

44.3. Import and export

ICalendarCodec reads and writes RFC 5545 VEVENT, VTODO, VALARM, attendees, recurrence rules, URI attachments, conference URIs, all-day values, and time zones. Unknown X- properties are retained in provider data.

String ics = ICalendarCodec.writeEvent(event);
CalendarEvent imported = ICalendarCodec.readEvent(ics);

44.4. Change notifications and offline synchronization

The cross-platform guarantee is intentionally pull based:

  • Local sources can emit CalendarChange through addChangeListener() when the operating system reports a local store change.

  • Online providers return a sync/delta token. Store it and pass it in the next CalendarQuery. The API doesn’t register provider webhooks.

Listeners run on the Codename One EDT. Treat a RESET change as an instruction to query again; it doesn’t promise one callback per underlying OS edit.

Offline mutation storage is opt-in. Supply a CalendarCache to CalendarSyncEngine, queue edits, and call sync() yourself or from the platform’s background-work callback. StorageCalendarCache stores calendar data and pending mutations, never credentials. Conflicts remain paused until the application chooses KEEP_LOCAL, KEEP_REMOTE, or MERGED.

CalendarSyncEngine sync = new CalendarSyncEngine(
        google, new StorageCalendarCache("google-account-1"));
sync.queueEventSave(event, CalendarMutationScope.ALL);
sync.sync().ready(result -> {
    for (CalendarConflict conflict : result.getConflicts()) {
        // Present both versions, then call resolveConflict(...).
    }
});

44.5. Permissions and build hints

Calling LocalCalendarSource.getInstance(), CalendarManager.getLocalSource(), CalendarManager.getSources(), or Display.getLocalCalendarSource() is the builder signal for local calendar integration. Android builds add READ_CALENDAR and WRITE_CALENDAR; iOS and macOS builds add EventKit and calendar/reminder privacy strings. You can replace the default user-facing strings with:

ios.NSCalendarsFullAccessUsageDescription=Explain why events are needed
ios.NSCalendarsWriteOnlyAccessUsageDescription=Explain why creating events is needed
ios.NSRemindersFullAccessUsageDescription=Explain why reminders are needed

The built-in Win32 source currently reports unavailable. An application-supplied native integration can explicitly opt into the standard MSIX uap:appointments capability with:

windows.msix=true
windows.calendar.restrictedCapability=true

Always check getCapabilities() and getAuthorizationStatus() at runtime. Manifest declarations make a request possible; they don’t mean the user or provider granted it.

45. External Surfaces: Widgets and Live Activities

Home-screen widgets and live activities answer the same developer question: how to keep a live piece of information outside the app. Codename One models both as one concept — an external surface — under the com.codename1.surfaces package. A widget is persistent, user-placed and content-driven: the weather, the next meeting, the delivery status on the home screen. A live activity is transient, app-started and progress-driven: the delivery that’s out right now, a running timer, a live score on the iOS lock screen and Dynamic Island, an ongoing Android notification, a floating desktop pill. Both share the same layout model, serialization, state mechanism, and action model.

The simulator renders every surface you publish in a dedicated preview window, so the full loop runs on your desktop with no device:

The simulator Widgets preview rendering the delivery widget with a native countdown and progress bar above the mock Dynamic Island

45.1. The dead-process rule

Surfaces render while your app process may not be running. Everything you hand this API is therefore turned into plain data at publish time: layouts serialize to a compact JSON descriptor, images are encoded to named PNG blobs, and "callbacks" are string action ids that open the app and reach your handler on the EDT. Layout text embeds ${key} placeholders resolved from a per-entry state map, so a content change is a cheap re-publish of data, not a new layout.

This rule explains everything unusual about the API. You can’t attach a listener to a widget — the process that renders it may have nothing to call back into. You can’t hand it a Component — there is no Codename One renderer on the other side. What you can do is describe the surface declaratively and let each platform’s native renderer (SwiftUI on iOS, RemoteViews on Android, a rasterizer on desktop) draw it.

45.2. Getting started

The build injects the native plumbing (a WidgetKit extension and App Group on iOS, widget receivers on Android) when your bytecode references com.codename1.surfaces. Apps that never touch the package get none of it, and on ports without surface support every entry point is an inert no-op.

Platform widget galleries are compiled into the native app, so widget kinds must be known at build time. Declare them in a surfaces.json resource (in src/main/resources of a Maven project, next to your icons):

{
  "liveActivities": true,
  "kinds": [
    {
      "id": "delivery_status",
      "name": "Delivery",
      "description": "Track your order",
      "iosFamilies": ["systemSmall", "systemMedium"],
      "androidMinWidthDp": 180,
      "androidMinHeightDp": 60
    },
    {
      "id": "analog_clock",
      "name": "Analog Clock",
      "description": "A vector clock face",
      "iosFamilies": ["systemSmall"],
      "androidMinWidthDp": 110,
      "androidMinHeightDp": 110
    }
  ]
}

The id values must match [a-z]. The families list accepts the portable names (small, medium, large, lockscreen, and the four watch complication families) as well as the WidgetKit spellings (systemSmall, systemMedium, systemLarge, accessoryRectangular); when omitted, all three home-screen sizes are offered. iosFamilies is the older spelling of the same key and still works — it predates there being a second platform that cared — and is read only when families is absent. The androidMinWidthDp / androidMinHeightDp / androidResizeMode fields fill the Android provider metadata. An optional top-level appGroup pins the iOS App Group id, and "liveActivities": true enables the live activity plumbing.

At runtime, mirror the manifest by registering each kind in your app’s init():

Surfaces.registerWidgetKind(new WidgetKind("delivery_status")
        .setDisplayName("Delivery")
        .setDescription("Track your order")
        .addSupportedSize(WidgetSize.SMALL)
        .addSupportedSize(WidgetSize.MEDIUM));

A widget renders a timeline: a shared layout plus dated entries whose state maps fill the layout’s ${key} placeholders. The OS flips entries on schedule without waking your app. This publishes a delivery tracker that advances through four states:

long now = System.currentTimeMillis();
long eta = now + 4 * 60000L;
WidgetTimeline timeline = new WidgetTimeline()
        .setContent(buildDeliveryLayout())
        .addEntry(new Date(now), deliveryState("Preparing your order", eta, 0.1f))
        .addEntry(new Date(now + 60000L), deliveryState("Out for delivery", eta, 0.4f))
        .addEntry(new Date(now + 3 * 60000L), deliveryState("Arriving now", eta, 0.9f))
        .addEntry(new Date(eta), deliveryState("Delivered", eta, 1f))
        .setReloadPolicy(WidgetTimeline.RELOAD_AT_END);
Surfaces.publish("delivery_status", timeline);

The layout itself is a small tree of nodes with shared styling (padding, background, corner radius, alignment, weight, size, action):

private SurfaceNode buildDeliveryLayout() {
    Map<String, Object> params = new HashMap<>();
    params.put("orderId", "CN1-12345");
    return new SurfaceColumn().setSpacing(6).setPadding(12)
            .add(new SurfaceRow().setSpacing(10)
                    .add(new SurfaceImage(courierAvatar)
                            .setSize(40, 40).setCornerRadius(20))
                    .add(new SurfaceColumn().setSpacing(2).setWeight(1)
                            .add(new SurfaceText("${status}")
                                    .setFontSize(15)
                                    .setFontWeight(SurfaceFontWeight.SEMIBOLD)
                                    .setMaxLines(1))
                            .add(new SurfaceDynamicText(
                                    SurfaceDynamicText.STYLE_TIMER_DOWN, "eta")
                                    .setFontSize(24)
                                    .setFontWeight(SurfaceFontWeight.BOLD)
                                    .setColor(SurfaceColor.ACCENT))))
            .add(new SurfaceProgress(SurfaceProgress.STYLE_LINEAR)
                    .setValueState("progress")
                    .setColor(SurfaceColor.rgb(ACCENT_COLOR)))
            .setAction("open_order", params);
}

private Map<String, Object> deliveryState(String status, long eta, float progress) {
    Map<String, Object> state = new HashMap<>();
    state.put("status", status);
    state.put("eta", eta);
    state.put("progress", progress);
    return state;
}

Three things in this layout carry the model’s weight. SurfaceText renders ${status} from the entry state, so updates ship a tiny state map instead of a layout. SurfaceDynamicText with STYLE_TIMER_DOWN is a countdown the OS animates on its own clock — the ETA ticks every second with zero app wake-ups. SurfaceProgress reads its value from the progress state key. The setAction call at the root makes a tap anywhere on the widget open the app and deliver the open_order action.

Surfaces.publish(…​) is callable from any thread and never blocks on the EDT, which matters for background refresh later in this chapter.

A complete runnable example lives in Samples/samples/SurfacesSample together with its surfaces.json:

The SurfacesSample main form with publish and live activity controls

45.2.1. Previewing in the simulator

Open Widgets > Widgets Preview in the simulator. The window lists your registered kinds, renders the published timeline of the selected kind at any size in light or dark mode, flips timeline entries on schedule, and ticks countdowns exactly as a home-screen widget would. The four watch complication families are listed too, at the accessory families' own sizes and clipped round where a watch face clips them — worth seeing, because a face shows nothing a circular complication draws into its corners. What it previews is the node tree, not the per-platform lowering, so a complication that looks right there can still arrive on a Wear OS face as one number. The mock Dynamic Island at the bottom renders running live activities. Clicks map through to your action handler, and a desktop (non-simulator) build renders the same publishes as floating widget windows pinned from a tray icon.

45.3. The node catalog

The catalog is kept small: a set of nodes every platform can render natively. Android app widgets (RemoteViews) are the constrained platform, so the catalog is designed to that floor and degrades as follows:

NodeiOS (SwiftUI)Android (RemoteViews)Notes

SurfaceColumn / SurfaceRow

VStack / HStack

LinearLayout

Weight maps to layout_weight

SurfaceBox

ZStack

FrameLayout

Child alignment via a 9-way enum

SurfaceText

Text

TextView

Android maps light/regular/medium weights to regular, semibold/bold to bold

SurfaceDynamicText timer

Text(date, style:)

Chronometer

Ticks natively on both

SurfaceDynamicText time

Text(date, style: .time)

TextClock

Native on both

SurfaceDynamicText date/relative

Native

Static text

Android refreshes it on the next update only

SurfaceImage

Image

ImageView

Named PNG blobs, content-hash dedup

SurfaceProgress linear

ProgressView

ProgressBar

Value 0..1 or a state key

SurfaceProgress circular

Gauge

Falls back to linear

Android widgets lack determinate circular progress

SurfaceProgress date interval

Native animation

Frozen at each refresh

iOS-only nicety

SurfaceVector

SwiftUI Canvas

Bitmap rendered in-process

Large vector nodes cost bitmap budget on Android

SurfaceSpacer

Spacer

Weighted View

Flexible gap

Corner radius

clipShape

Background drawable

May render square below Android 12

Node action

Link / widgetURL

setOnClickPendingIntent

Small iOS widgets honor only the root action

Descriptors are limited to 8 nesting levels. Keep payloads (JSON plus images) comfortably under 200kb — the iOS widget extension runs in about 30mb of memory, and Android parcels rendered widgets over a 1mb binder transaction.

45.4. Vector widgets

SurfaceVector covers the widgets the template catalog can’t express — clocks, gauges, dials — with a retained list of vector drawing operations (fills, strokes, lines, arcs, text, rotation groups) replayed natively by every renderer. Rotation groups can read their angle from a state key, which turns a static drawing into a data-driven one.

The classic example is an analog clock. The face is resolution independent — the 200x200 view box scales to whatever size the widget gets:

private SurfaceNode buildClockFace() {
    SurfaceVector face = new SurfaceVector(200, 200)
            .fillEllipse(100, 100, 96, 96, SurfaceColor.rgb(0xfffafafa, 0xff1c1c1e))
            .strokeEllipse(100, 100, 96, 96, 4, SurfaceColor.rgb(ACCENT_COLOR));
    for (int hour = 0; hour < 12; hour++) {
        boolean quarter = hour % 3 == 0;
        face.beginRotation(hour * 30f, 100, 100)
                .line(100, 10, 100, quarter ? 24 : 18, quarter ? 5 : 3, SurfaceColor.LABEL)
                .endRotation();
    }
    face.beginRotation("hourAngle", 100, 100)
            .line(100, 100, 100, 52, 8, SurfaceColor.LABEL)
            .endRotation()
            .beginRotation("minuteAngle", 100, 100)
            .line(100, 100, 100, 26, 5, SurfaceColor.rgb(ACCENT_COLOR))
            .endRotation()
            .fillEllipse(100, 100, 6, 6, SurfaceColor.rgb(ACCENT_COLOR));
    return face;
}

Angles use the clock convention: degrees, 0 at 12 o’clock, clockwise positive. The face publishes once; 60 per-minute timeline entries carry only the two angle values, and the OS flips them on schedule. The clock stays correct for an hour with zero app wake-ups:

WidgetTimeline timeline = new WidgetTimeline()
        .setContent(buildClockFace())
        .setReloadPolicy(WidgetTimeline.RELOAD_AT_END);
Calendar c = Calendar.getInstance();
c.set(Calendar.SECOND, 0);
c.set(Calendar.MILLISECOND, 0);
long minuteStart = c.getTimeInMillis();
int baseMinutes = c.get(Calendar.HOUR_OF_DAY) * 60 + c.get(Calendar.MINUTE);
for (int m = 0; m < 60; m++) {
    int totalMinutes = baseMinutes + m;
    Map<String, Object> state = new HashMap<>();
    // clock convention: degrees, 0 = 12 o'clock, clockwise positive
    state.put("minuteAngle", totalMinutes % 60 * 6f);
    state.put("hourAngle", totalMinutes % 720 * 0.5f);
    timeline.addEntry(new Date(minuteStart + m * 60000L), state);
}
Surfaces.publish("analog_clock", timeline);
The analog clock widget rendered from a SurfaceVector in the simulator preview

45.5. Live activities

A live activity is started explicitly from the app and updated by pushing fresh state. The descriptor carries the main content (the lock screen presentation on iOS, the notification layout on Android) plus the Dynamic Island regions: setCompactLeading / setCompactTrailing for the pill shown around the camera cutout, setMinimal for the detached circle when several activities run, and setExpandedLeading / setExpandedTrailing / setExpandedCenter / setExpandedBottom for the long-press expanded card.

long eta = System.currentTimeMillis() + 4 * 60000L;
LiveActivityDescriptor descriptor = new LiveActivityDescriptor("delivery")
        .setContent(buildDeliveryLayout())
        .setCompactLeading(new SurfaceImage(courierAvatar)
                .setSize(24, 24).setCornerRadius(12))
        .setCompactTrailing(new SurfaceDynamicText(
                SurfaceDynamicText.STYLE_TIMER_DOWN, "eta")
                .setFontSize(14).setColor(SurfaceColor.ACCENT))
        .setExpandedCenter(new SurfaceText("${status}")
                .setFontSize(16).setFontWeight(SurfaceFontWeight.SEMIBOLD))
        .setExpandedBottom(new SurfaceProgress(SurfaceProgress.STYLE_LINEAR)
                .setValueState("progress")
                .setColor(SurfaceColor.rgb(ACCENT_COLOR)))
        .setTint(SurfaceColor.rgb(ACCENT_COLOR));
activity = LiveActivity.start(descriptor,
        deliveryState("Out for delivery", eta, 0.25f));

The returned handle is inert (isActive() returns false) on platforms without live activity support, so no platform checks are needed. Updates and the final state are plain state maps interpolated into the descriptor persisted at start time:

activity.update(deliveryState("Arriving now", eta, 0.9f));
// ... and when the delivery completes:
activity.end(deliveryState("Delivered", System.currentTimeMillis(), 1f));

The simulator preview renders the running activity as a mock Dynamic Island pill plus the expanded card:

The simulator’s mock Dynamic Island pill and expanded live activity card

On iOS the activity appears on the lock screen and, on supported devices, inside the Dynamic Island (ActivityKit requires iOS 16.1 or newer). On Android it lowers to an ongoing notification that renders the same content; on Android 13 and newer the first start(…​) prompts for the notification permission, which the build declares for you, and isSupported() reports false once the user has turned that prompt down twice or has switched notifications off for the app. On desktop it appears in the simulator preview or as a floating window.

45.6. Actions and cold start

Every interactive node carries a string action id plus an optional parameter map, set with setAction(id, params). Tapping the surface opens the app and delivers a SurfaceActionEvent to the single handler registered with Surfaces.setActionHandler(…​):

Surfaces.setActionHandler(evt -> {
    if ("open_order".equals(evt.getActionId())) {
        String orderId = (String) evt.getParams().get("orderId");
        showOrderForm(orderId);
    } else {
        Dialog.show("Surface Action", "Action " + evt.getActionId()
                + " from " + evt.getSource()
                + ", cold start: " + evt.isColdStart(), "OK", null);
    }
});

Actions are delivered on the EDT. When the tap launches a dead app, the event is queued across the cold start and delivered once your handler registers — which is why the registration belongs in init(). SurfaceActionEvent.isColdStart() tells you which path you are on, getSource() identifies the surface (widget kind or activity type), and getParams() returns the parameter map serialized with the action.

45.7. Updating from the background

Surfaces are updated from the running app in this release; push-driven updates are planned. Three mechanisms keep content fresh without keeping the app open:

Timelines carry the future with them. Publish entries covering the hours ahead and the OS flips them on schedule — the clock above stays correct for an hour without a single wake-up. WidgetTimeline.RELOAD_AT_END (the default) asks the platform to request fresh content when the entries run out.

Background fetch re-publishes data. Implement com.codename1.background.BackgroundFetch, register with Display.setPreferredBackgroundFetchInterval(int), and re-publish inside the callback — publishing is data-only file IO, safe from any thread and from a UI-less process:

public void performBackgroundFetch(long deadline, Callback<Boolean> onComplete) {
    // fetch fresh data, then re-publish the timeline
    publishDeliveryTimeline();
    onComplete.onSucess(Boolean.TRUE);
}

The completion callback must be invoked, or iOS stops granting fetch time. On iOS, background fetch also needs the fetch background mode:

codename1.arg.ios.background_modes=fetch

On Android, the widget pulls the app. When a provider renders an exhausted RELOAD_AT_END timeline (or no timeline at all) it starts the background fetch service directly — throttled to once per 15 minutes per kind, and only when the app declares background fetch. The iOS widget extension can’t wake the app arbitrarily: an exhausted timeline makes WidgetKit re-read the persisted document, so publish timelines with enough future entries to bridge the gap between fetches. The simulator simulates background fetch with a timer that fires while the app is paused (Simulate > Pause App).

45.8. Per-platform notes

45.8.1. iOS

The build generates a CN1Widgets WidgetKit extension target into the Xcode project and shares published timelines with it through an App Group container. The App Group id defaults to group.<your package name>; override it with the appGroup field in surfaces.json or the ios.surfaces.appGroup build hint. The extension’s deployment target defaults to 16.1 (ActivityKit’s floor) without raising your app’s own deployment target — on older iOS versions the extension never runs and Surfaces.areWidgetsSupported() returns false.

Signing follows the generic app extension rules: the extension needs its own App ID (<your package>.CN1Widgets) with the App Groups capability and, for cloud device builds with manual signing, its own provisioning profile. Either place the .mobileprovision under common/src/main/resources or supply a URL with the ios.appext.CN1Widgets.provisioningURL build hint. With automatic (managed) signing and in local ios-source builds no extra configuration is needed — Xcode provisions the extension target as usual.

Like the app itself (codename1.ios.debug.provision / codename1.ios.release.provision), the extension needs a development profile for debug device builds and a distribution profile for release builds. Qualify any extension provisioning key with debug or release after the ios. segment and the build picks the variant matching the build target, falling back to the unqualified key when no matching variant is set:

  • Build hints: codename1.arg.ios.debug.appext.CN1Widgets.provisioningURL and codename1.arg.ios.release.appext.CN1Widgets.provisioningURL

  • Local profile paths in codenameone_settings.properties: codename1.ios.debug.appext.CN1Widgets.provision and codename1.ios.release.appext.CN1Widgets.provision

The Certificate Wizard populates both provision variants automatically when it sets up widget extension signing (the development profile requires at least one registered device). An unqualified key alone still works and is used for both build types.

Widget taps deep link back into the app through the cn1surface:// URL scheme, which the build registers automatically.

45.8.2. Android

Widgets are rendered through RemoteViews by generated per-kind providers; no Android-specific build hints are needed, and the per-kind sizing metadata comes from surfaces.json. Timeline entry flips are scheduled with inexact alarms (a 30-second window) to avoid the exact-alarm permission by default; apps that need to-the-second flips can opt in with the android.surfaces.exactAlarms build hint. Second-precision countdowns still tick natively through Chronometer. Live activities lower to ongoing notifications, which on Android 13 and newer require the POST_NOTIFICATIONS runtime permission: the build declares it for you when surfaces.json sets "liveActivities": true, and the first LiveActivity.start(…​) raises the system prompt, blocking the calling thread until the user answers. Codename One raises it at most twice across an install — Android stops showing the dialog after two refusals anyway — and spends an attempt only on a request it managed to issue. Android reports a dismissed dialog exactly as it reports a refusal, so dismissing one does cost an attempt, but one rather than the whole budget. Start the first activity while your app is in the foreground: there is no UI to prompt from in a background service or push handler, so a start from one before the permission is granted is refused without spending an attempt. LiveActivity.isSupported() is the programmatic signal once the answer has settled — a spent budget or, for an app that never declared the permission, a missing manifest entry — and adb logcat -s CN1Surfaces explains every refusal, reporting each settled reason once. A grant from anywhere counts — these prompts, push registration, Display.requestNotificationPermission(…​) or the system settings — because the live permission state is always checked first. The approximations listed in the node catalog table apply: font weights collapse to regular/bold, circular progress falls back to linear, relative dates refresh only on entry flips, and vector nodes render as bitmaps.

45.8.3. Apple Watch

A kind declaring a WATCH_* family gets a second WidgetKit extension, CN1WatchWidgets, embedded in the watch app rather than the phone app. It needs watchOS 10 (watchNative.surfaces.deploymentTarget overrides the floor), and it shares the App Group identifier with the phone — though the container behind it is watch-local, which is why the watch publishes its own timelines. Under manual signing the extension’s bundle id, <your.package>.watchkitapp.CN1WatchWidgets, needs its own provisioning profile; the generic ios.appext.CN1WatchWidgets.provisioningURL hint supplies it.

45.8.4. Wear OS

A watch-bearing kind gets a ComplicationDataSourceService, and the WATCH_RECTANGULAR family additionally gets a TileService. Both are generated into the Wear module — the single APK in a standalone build, the wear module in a companion one — and pull in the androidx.wear complication and Tile libraries, which are AndroidX-only and raise that module’s minSdkVersion to 26. The phone module’s floor is untouched.

The node catalog maps as follows. A complication is the lossy one: the face asks for a typed value and composes it itself.

NodeComplicationTile

SurfaceText / SurfaceDynamicText

First two nodes only, as text and title

Rendered, but a countdown is frozen and refreshed on timeline flips

SurfaceImage / SurfaceVector

First node only, as a monochrome glyph the face tints

Rendered as an inline image resource

SurfaceProgress

Becomes the ranged value

Renders natively as an arc — better than the phone widget, which degrades a circular bar to linear

SurfaceRow / SurfaceColumn / SurfaceBox

Traversal order only; there is no layout to honour

Rendered

Padding, background, alignment, weight, colour

Dropped — the face owns its design

Rendered

Actions

Root action only, as the complication tap

Per-node actions work — better than a small iOS widget

Everything a complication drops is logged once per render; adb logcat -s CN1Surfaces shows what a face is actually displaying.

45.8.5. Desktop, Windows, and Linux

In a desktop build the app shows a tray icon whose menu pins a floating widget per kind: a frameless, always-on-top window rendering the published timeline, with clicks dispatched to your action handler. Window positions and the pinned set persist across runs, and a running live activity docks a pill window at the top of the primary screen. Desktop widgets are process-bound in this release — they exist while the app process runs. On Windows the plain signed executable ships these layered floating widgets with zero packaging; setting windows.msix=true additionally wraps the build in an MSIX package that declares a Windows 11 Widgets Board provider, so your kinds appear in the Win+W board rendered as Adaptive Cards. The MSIX channel is opt-in because it has real distribution prerequisites: a certificate the target machine trusts, the Windows App Runtime redistributable on the target machine, and Windows 11 for the board itself. On Linux the widgets are frameless GTK applet windows; on Wayland compositors that support the layer-shell protocol (KDE Plasma, Sway and the rest of the wlroots family) a runtime-loaded gtk-layer-shell places widgets above the wallpaper as real desktop applets with persistent positions and drag-to-move, and on GNOME Wayland they degrade to plain floating windows because the compositor controls global positioning and keep-above.

45.9. Build hints

HintDefaultPurpose

ios.surfaces.extension

true

Set to false to skip the whole iOS lowering: no extension, no App Group, and the surfaces API reports unsupported at runtime

ios.surfaces.appGroup

group.<package>

The shared App Group id; the appGroup field in surfaces.json takes precedence

ios.surfaces.deploymentTarget

16.1

Deployment target of the widget extension (the host app is unaffected)

ios.surfaces.frequentUpdates

false

Adds NSSupportsLiveActivitiesFrequentUpdates for high-frequency live activity updates

ios.appext.CN1Widgets.provisioningURL

URL of the extension’s provisioning profile for cloud manual-signing builds; used for both debug and release builds unless a qualified variant is set

ios.debug.appext.CN1Widgets.provisioningURL / ios.release.appext.CN1Widgets.provisioningURL

Build-type-specific variants: the URL of the development profile used by debug device builds and the URL of the distribution profile used by release builds. The variant matching the build target overrides the unqualified hint

ios.background_modes

Add fetch so background fetch can re-publish timelines on device

android.surfaces.exactAlarms

false

Schedule widget timeline entry flips with exact alarms; declares SCHEDULE_EXACT_ALARM and falls back to the inexact 30-second window when the user revokes the special app access

watchNative.surfaces.deploymentTarget

10.0

Deployment target of the watchOS complication extension. Can’t go below 10.0: the container background every generated widget applies is watchOS 10, so a lower floor fails the build rather than losing the background

ios.appext.CN1WatchWidgets.provisioningURL

URL of the watch complication extension’s provisioning profile, for cloud manual-signing builds. The ios.debug. / ios.release. variants work as they do for CN1Widgets

android.surfaces.complicationUpdateSeconds

0

How often the system polls a Wear complication data source. Zero means never: the timeline model is push-driven, so a poll spends watch battery asking a question the app has already answered

android.wear.complicationsVersion / android.wear.tilesVersion / android.wear.protoLayoutVersion

pinned

Override the androidx.wear library versions the generated complication and Tile services compile against

android.watchModule

true

Set to false to keep the wearable link without generating a companion Wear module; the phone build is then unchanged

android.watchVersionCode / android.watchVersionCodeOffset

+100000000

The Wear artifact’s version code, which must outrank the phone’s for Play to pick it on a watch. The offset is wide so the phone’s next release can’t catch up to a watch code already published

windows.msix

false

Wrap the Windows build in an MSIX package with a Widgets Board provider

windows.msix.identityName

package name

MSIX package identity name

windows.msix.publisher

CN=<display name>

MSIX identity publisher; must match the signing certificate subject

windows.msix.version

1.0.0.0

MSIX package version

windows.msix.pfx / windows.msix.password

executable signing configuration

Certificate used to sign the MSIX package

45.10. Current limitations

  • Updates originate from the app (timelines, background fetch, live activity updates). Server-pushed widget content and ActivityKit push tokens are planned; the wire format already accommodates them.

  • The node catalog is intentionally the lowest common denominator — there is no arbitrary per-pixel drawing beyond SurfaceVector, and no embedding of regular Codename One components.

  • WidgetSize.LOCKSCREEN maps to the iOS accessoryRectangular family and is ignored on Android in this release.

  • A kind declaring only WATCH_* families produces no phone surface on either platform — those kinds are hosted by the watch. Declare a phone family alongside them if you want both.

  • What a watch face shows is narrower than what you laid out, and on Wear OS much narrower. See the wearables chapter for the per-family mapping and what gets dropped.

  • The Widgets Board provider requires the windows.msix opt-in and its distribution prerequisites; without it, Windows desktop widgets are floating windows.

46. Maps

Codename One ships a modern mapping API in the com.codename1.maps package built around two components:

  • MapView — a pure-vector map drawn entirely through the Codename One Graphics pipeline. It never embeds a native peer, so it composes cleanly with the rest of your UI (dialogs, lists and overlays draw over it) and behaves identically on every platform, including the simulator and the web.

  • NativeMap — a map backed by a native provider (Apple MapKit, Google Maps, …​) when one is wired into the build, and which transparently falls back to a MapView when no provider is available (the simulator, devices without the selected provider, or builds that didn’t opt in).

Both components implement the same MapSurface interface, so application code is identical regardless of which one — or which provider — is backing the map.

The legacy tile-based MapComponent and the external codenameone-google-maps cn1lib are deprecated in favor of this API.

46.1. A first map

The pure-vector MapView renders real maps with zero configuration and no API key — by default it shows the free, keyless OpenFreeMap vector basemap (real OpenStreetMap data):

MapView map = new MapView();
map.moveCamera(new LatLng(37.7749, -122.4194), 12);
form.add(BorderLayout.CENTER, map);

LatLng is the immutable WGS84 coordinate value type used throughout the API. The camera is described by a center LatLng and a fractional zoom level (the standard slippy-map scale where each whole increment doubles the scale).

The pure-vector MapView rendering OpenStreetMap data

46.2. The MapSurface API

Every map — vector or native — exposes the same operations through MapSurface:

// Camera
map.setCameraPosition(new CameraPosition(new LatLng(48.8566, 2.3522), 11));
map.moveCamera(new LatLng(48.8566, 2.3522), 11);
map.setZoom(13);
map.fitBounds(new MapBounds(new LatLng(48.8, 2.2), new LatLng(48.9, 2.4)), 24);

// Markers
Marker m = map.addMarker(new MarkerOptions(new LatLng(48.8584, 2.2945))
        .icon(pinImage)
        .title("Eiffel Tower")
        .anchor(0.5f, 1.0f)
        .onClick(e -> showDetails()));
map.removeMarker(m);

// Shapes
map.addPolyline(new Polyline(routePoints).setStrokeColor(0xff5722).setStrokeWidth(6));
map.addPolygon(new Polygon(areaPoints).setFillColor(0x803f51b5).setStrokeColor(0x3f51b5));
map.addCircle(new Circle(new LatLng(48.85, 2.35), 500).setFillColor(0x804caf50));
map.clearMapObjects();

// Coordinate conversion and bounds
Point pixel = map.latLngToScreen(new LatLng(48.85, 2.35));
LatLng coord = map.screenToLatLng(120, 240);
MapBounds visible = map.getVisibleRegion();

// Events
map.addTapListener((surface, location, x, y) -> placeMarker(location));
map.addLongPressListener((surface, location, x, y) -> contextMenu(location));
map.addCameraChangeListener((surface, camera) -> persist(camera));

Marker icons are supplied as EncodedImage and anchored in normalized (u, v) image space (0.5, 1.0 puts the pin tip on the location). When no icon is given, a marker draws the standard Material Design map pin. Polygon and circle fills accept an 0xAARRGGBB color so you can make them translucent.

Markers drawn with the default Material map pin

46.3. Routes that follow the road

A polyline joins the coordinates you hand it with straight lines — it knows nothing about roads. Passing two endpoints to addPolyline therefore draws a single straight segment across the map, which is seldom what a navigation screen wants. Working out the road geometry between two points is a separate job, done by a routing service, and that’s what the com.codename1.maps.routing package is for:

MapView map = new MapView();
form.add(BorderLayout.CENTER, map);

// Draws the driving route along the actual roads and frames it.
Routing.showRoute(map, origin, destination);

Routing.showRoute(…​) fetches the route, draws it and moves the camera to frame it. Like the keyless OpenFreeMap basemap, it works with no API key and no signup: routing runs over OpenStreetMap data through the built-in OsrmRouteService.

Routing is asynchronous — the call returns immediately and the line appears when the service answers. Use Routing.findRoute(…​) when you want the result itself, for example to style the line or show the distance and the estimated time:

RouteRequest request = new RouteRequest(origin, destination)
        .addWaypoint(new LatLng(38.8899, -77.0091))
        .setTravelMode(TravelMode.DRIVING);

Routing.findRoute(request, new RouteCallback() {
    @Override
    public void routesFound(java.util.List routes) {
        Route best = (Route)routes.get(0);
        map.addPolyline(best.toPolyline().setStrokeColor(0xff5722).setStrokeWidth(6));
        map.fitBounds(best.getBounds(), CN.convertToPixels(4));
        label.setText((int)(best.getDistanceMeters() / 1000) + " km, "
                + (int)(best.getDurationSeconds() / 60) + " min");
    }

    @Override
    public void routeFailed(String message, Throwable error) {
        label.setText(message);
    }
});

A Route carries the drawable geometry (toPolyline()), the bounds to frame it with, the total distance and duration, and the RouteLeg/RouteStep breakdown behind a turn-by-turn list. A RouteRequest adds waypoints and a TravelMode (DRIVING, WALKING, CYCLING).

The default endpoint is OSRM’s public demo server. It exists for development and demos: no SLA, rate limited, and it hosts the car profile, so walking and cycling requests are routed over driving data there. Before shipping, point the service at your own OSRM instance (or any OSRM-compatible endpoint) — nothing else in your code changes:
// Your own OSRM instance, or any OSRM-compatible endpoint.
Routing.setService(new OsrmRouteService("https://osrm.example.com"));

To route through a different provider entirely — Google Directions, Mapbox Directions, GraphHopper, your own server — implement RouteService and install it the same way. Every provider of that kind returns the route as an encoded polyline, which Polyline.fromEncoded(geometry) turns straight into something drawable (PolylineCodec also handles the polyline6 variant used by Valhalla).

46.4. Tile sources and styles (MapView)

MapView pulls its tiles from a pluggable com.codename1.maps.vector.TileSource:

  • MvtTileSource — networked Mapbox Vector Tiles (.pbf/.mvt). MvtTileSource.openFreeMap() is the free, keyless OpenStreetMap-based default (its tile URLs are versioned, so it resolves them from OpenFreeMap’s TileJSON automatically). Most other hosted vector basemaps require an API key supplied via setApiKey(…​) and referenced as {key} in the URL template; a TileJSON endpoint (a URL with no {z} token) is resolved automatically.

  • RasterTileSource — networked XYZ image tiles. RasterTileSource.openStreetMap() is a keyless raster alternative.

  • BundledTileSource — tiles bundled into the app as resources, for fully offline maps. The screenshot tests in this repo use this against a small fixture set so the renders are deterministic and network-free; it’s not how you ship a real map.

  • DemoTileSource — a self-contained synthetic tile set (no network), handy for demos and deterministic tests.

46.4.1. Connecting to real map data

For a real app you point MapView at a hosted tile service. The simplest keyless option is OpenFreeMap (OpenStreetMap-based vector tiles); for branded styles or higher quotas use a keyed provider such as MapTiler or your own self-hosted Protomaps/TileServer GL endpoint:

// Keyless, zero-config vector basemap (OpenStreetMap data via OpenFreeMap):
MapView vector = new MapView(MvtTileSource.openFreeMap(), MapStyle.light());

// A keyed hosted provider (e.g. MapTiler). {z}/{x}/{y} are the XYZ tile
// coordinates and {key} is substituted from setApiKey(...):
MapView branded = new MapView(
        new MvtTileSource("https://api.maptiler.com/tiles/v3/{z}/{x}/{y}.pbf?key={key}", 0, 14)
                .setApiKey(apiKey),
        MapStyle.dark());

// A keyless raster (image-tile) basemap, if you don't need vector styling:
MapView raster = new MapView(RasterTileSource.openStreetMap());

If the URL has no {z} token it’s treated as a TileJSON document and the real tile-URL template (and min/max zoom) are resolved from it automatically, so you can paste a provider’s TileJSON URL directly.

To bundle your own offline tiles instead, drop .mvt/.pbf files into the app’s resources under a {z}/{x}/{y} layout and point BundledTileSource at them — that’s exactly what the test fixtures do.

Vector tiles are painted according to a MapStyle. The built-in MapStyle.light() and MapStyle.dark() cover a usable basemap; MapStyle.fromJson(json) parses a subset of the MapLibre GL style specification (background/fill/line/symbol layers, zoom-stop interpolation, simple ["==", key, value] filters).

The same vector data rendered with the built-in dark style

46.4.2. Street names and street-level zoom

Street names are written along the streets themselves, the way a printed street map shows them. A name follows the line of its road, bends with it on a gentle curve and always reads left to right. A long road repeats its name at intervals so a name is in view wherever you look along it. A name is left out rather than scrambled where a corner falls under it, where the road is shorter than the name, or where it would overlap another label. Place names and points of interest stay horizontal.

Labels are drawn above the lines and shapes you add and below the markers. A route drawn with addPolyline therefore never hides the names of the streets it follows, while pins stay on top of everything:

A route drawn over the vector basemap with the street names laid along the roads and above the route

When you draw on the basemap yourself through getEngine(), the engine’s paint method draws the tiles and their labels together. To put your own drawing between the two, call paintTiles, draw, then call paintLabels.

A vector map zooms further than the deepest level its tile source serves. OpenFreeMap’s data stops at zoom 14, yet the map reaches zoom 20: past zoom 14 the map redraws the zoom 14 geometry at the larger scale, so roads and buildings stay sharp instead of turning into a stretched image. getMaxZoom() reports the deepest level the camera can reach: six levels past the source’s own, up to zoom 22. A source that serves deeper tiles than that keeps its own deepest level. A raster source stops at its own deepest level, since enlarging an image only blurs it.

Street-level zoom on the vector basemap with road names repeated along the streets

Line widths, like text sizes, are in logical pixels and scale with the screen density, so a road is as thick on a high-density phone as on a desktop. The built-in styles keep widening roads as you zoom in to street level. At the deepest zooms a street name can sit a little to the side of its road: the name comes from a simplified copy of the road’s line in the tile data, and the difference grows as the map enlarges it.

46.5. Native maps and providers

NativeMap renders through a native map SDK when the build wires one in:

NativeMap map = new NativeMap(new LatLng(37.7749, -122.4194), 12);
map.addMarker(new MarkerOptions(new LatLng(37.7749, -122.4194)).title("San Francisco"));
form.add(BorderLayout.CENTER, map);

if (!map.isNativeMap()) {
    // Running on the simulator (or a build without a provider) -> vector fallback.
}
NativeMap backed by Apple MapKit (ios.maps.provider=apple)

Which provider (if any) backs the map is decided entirely by build hints — the public API never names a provider, so unused providers add nothing to your app’s size. Select one with the maps.provider build hint (or the per-platform ios.maps.provider / android.maps.provider):

# iOS uses Apple MapKit (a free system framework, no API key, no pod):
codename1.arg.ios.maps.provider=apple

# Android uses Google Maps (pulls Google Play Services Maps):
codename1.arg.android.maps.provider=google

# ...or Huawei Map Kit, for devices without Google Play Services (HMS Core).
# The build pulls com.huawei.hms:maps from Huawei's Maven repo automatically;
# your app still needs the usual HMS onboarding (agconnect-services.json / key):
codename1.arg.android.maps.provider=huawei

The built-in providers are Apple MapKit (iOS), Google Maps (Android) and Huawei Map Kit (Android). Huawei Map Kit runs on any Android device with HMS Core installed — not only Huawei hardware — so it’s the natural choice for the Android market without Google Play Services. Each reports isAvailable() at runtime (Google checks Play Services, Huawei checks HMS Core), and when the selected provider is unavailable NativeMap falls back to the pure-vector MapView.

When a provider is selected the build server injects that provider’s implementation into your app and wires it in; with no provider selected (or when the provider is unavailable at runtime, for example when Google Play Services is missing) NativeMap simply renders the vector MapView fallback. You can configure the fallback basemap explicitly:

NativeMap map = new NativeMap(new LatLng(0, 0), 4, fallbackTileSource, MapStyle.light());

46.5.1. Provider API keys

Apple MapKit needs no key. Google Maps requires the usual keys, supplied as build hints:

codename1.arg.android.xapplication=<meta-data android:name="com.google.android.maps.v2.API_KEY" android:value="YOUR_ANDROID_API_KEY"/>
codename1.arg.ios.afterFinishLaunching=[GMSServices provideAPIKey:@"YOUR_IOS_API_KEY"];

46.5.2. Cross-platform web maps

A native provider only renders on the platforms that ship its SDK. To show a provider’s map everywhere — including platforms with no native SDK for it — register a WebMapProvider, which hosts the provider’s JavaScript SDK inside a BrowserComponent. It draws the basemap and not much else, though: the camera getters report zero and markers, shapes, coordinate conversion and gestures are no-ops, so prefer it only where a static map is what you want:

// Render Google Maps through its JavaScript SDK on any platform with a browser:
MapProviderRegistry.register(WebMapProvider.google("YOUR_MAPS_JS_API_KEY"));

// Registering only appends to the registry, and a build-injected native
// provider already there wins -- Apple Maps on iOS, for one. Asking for
// the web provider by name is what makes this one render everywhere.
//
// Ask for it only when a basemap is all you need. WebMapProvider draws
// the tiles and little else: the camera getters answer zero, and
// markers, shapes, coordinate conversion and the gesture callbacks are
// no-ops. setZoom() through it reads that zero position and recenters on
// (0, 0). For the operations the MapSurface section shows, leave the
// native provider in front.
MapProviderRegistry.setPreferredProvider("web");

NativeMap map = new NativeMap(new LatLng(41.0, 13.0), 5);

Because it needs only a web view, the web provider (id web) is the natural last step before the pure-vector fallback in a provider chain. The order is fully customizable from code, so you can express per-platform preferences — for example "try the native Google SDK, then the web map, then the vector `MapView`":

MapProviderRegistry.setProviderOrder(new String[]{"google", "web", "vector"});
Google Maps rendered cross-platform through WebMapProvider

46.6. Choosing between MapView and NativeMap

Use MapView when you want a lightweight, dependency-free map that looks the same everywhere and composes with CN1 components. Use NativeMap when you want the platform’s native map look, gestures and performance, and are willing to opt a provider in through build hints — with the guarantee that it still works (as a vector map) where the provider is unavailable.

47. In-Car Experiences (CarPlay & Android Auto)

Codename One can project a driver-safe UI onto Apple CarPlay and Google Android Auto from a single portable API under the com.codename1.car package. You describe the screens once with a small set of cross-platform templates; Codename One renders them as native CPTemplate`s on CarPlay and `androidx.car.app templates on Android Auto.

CarPlay and Android Auto are template-based UI systems. Unlike the phone, tablet, or even Apple TV/Android TV (which reuse the normal Codename One renderer), the car platforms don’t let an app draw arbitrary pixels or show a regular Codename One Form. To minimise driver distraction the system only renders a fixed catalogue of templates — lists, grids, info panes, messages, navigation and now-playing — and grants access per app category (audio, communication, navigation, point-of-interest). The com.codename1.car API maps the common in-car use cases onto that catalogue. "Seamless" here means seamless integration, lifecycle and build wiring — not running your existing phone UI on the dashboard.

This API is zero cost when unused: simply referencing com.codename1.car is what tells the build to wire the native plumbing (the CarPlay scene + entitlement on iOS, the androidx.car.app dependency + CarAppService on Android). Apps that never touch the package get none of it.

47.1. Quick start

Register a single CarApplication from your app’s init() — before a head unit connects — and return a root CarScreen that builds a template:

public class MyApp {
    public void init(Object context) {
        // ... normal app init ...
        Car.setApplication(new MyCarApplication());
    }
}

class MyCarApplication extends CarApplication {
    public CarScreen onCreateRootScreen(CarContext context) {
        return new LibraryScreen();
    }
}

class LibraryScreen extends CarScreen {
    protected CarTemplate onCreateTemplate() {
        return new CarListTemplate().setTitle("Library")
            .addRow(new CarRow("Now Playing").setText("Daft Punk - Discovery")
                .setOnAction(ctx -> play()))
            .addRow(new CarRow("Albums").setBrowsable(true)
                .setOnAction(ctx -> ctx.pushScreen(new AlbumsScreen())))
            .addRow(new CarRow("Playlists").setBrowsable(true)
                .setOnAction(ctx -> ctx.pushScreen(new PlaylistsScreen())));
    }
}

That single description renders natively on both platforms:

CarPlay list template

On the simulator and on any port without in-car projection there is no head unit, so the whole API is an inert no-op and CN.isCarConnected() returns false — your code never needs platform if statements.

47.2. Templates

A CarScreen returns exactly one CarTemplate. The catalogue mirrors what the head units allow:

TemplateUse forCarPlayAndroid Auto

CarListTemplate

Sectioned scrolling list of rows (browse, settings)

CPListTemplate

ListTemplate

CarGridTemplate

Image-forward grid (categories, presets, artwork)

CPGridTemplate

GridTemplate

CarPaneTemplate

Detail pane: label/value rows + up to two actions

CPInformationTemplate

PaneTemplate

CarMessageTemplate

Short message + actions (errors, prompts, messaging)

CPInformationTemplate

MessageTemplate

CarNavigationTemplate

Turn-by-turn surface with a drawable map

CPMapTemplate

NavigationTemplate

CarNowPlayingTemplate

System now-playing surface for audio apps

CPNowPlayingTemplate

media now-playing

Rows (CarRow), grid items (CarGridItem) and actions (CarAction) carry a title, optional secondary text, an optional com.codename1.ui.Image icon and a CarActionListener invoked on the EDT when the element is selected. Mark a row setBrowsable(true) when selecting it drills into a deeper screen.

Android Auto grid template

Head units enforce a hard cap on the number of rows/items they display (driver-distraction rules). Query it and trim accordingly:

int max = context.getListRowLimit(); // 0 when the head unit doesn't say
int count = (max > 0) ? Math.min(items.size(), max) : items.size();
CarListTemplate t = new CarListTemplate().setTitle("Library");
for (int i = 0; i < count; i++) {
    t.addRow(new CarRow(items.get(i)));
}

47.3. Screen stack & lifecycle

CarContext manages a back stack, mirroring androidx.car.app’s `ScreenManager and CarPlay’s CPInterfaceController:

context.pushScreen(new DetailScreen());  // drill in
context.popScreen();                     // back
screen.invalidate();                     // rebuild this screen's template after a model change
context.showToast("Added to queue");

CarScreen exposes optional lifecycle hooks — onCreate(), onResume(), onPause(), onDestroy() — and CarApplication is notified of connection changes via onCarConnected(CarContext) / onCarDisconnected(). You can also observe connection globally:

class CarAwareForm extends Form {
    // Car keeps the listener in a static list, so a screen-scoped observer
    // has to hand back this exact instance or every recreated screen stays
    // reachable and keeps receiving connection callbacks
    private CarConnectionListener connectionListener;

    protected void initComponent() {
        super.initComponent();
        connectionListener = new CarConnectionListener() {
            public void carConnected(CarContext ctx) { startLocationStream(); }
            public void carDisconnected()           { stopLocationStream(); }
        };
        Car.addConnectionListener(connectionListener);
    }

    protected void deinitialize() {
        Car.removeConnectionListener(connectionListener);
        super.deinitialize();
    }
}

47.4. Now-playing (audio apps)

For audio apps the track metadata, artwork, scrubber and transport controls are driven by the platform media session (the Android background audio service / MPNowPlayingInfoCenter on iOS), so CarNowPlayingTemplate carries no metadata of its own — it routes the head unit to the now-playing surface and lets you add a couple of app-specific buttons (shuffle, like).

Now-playing / pane template

47.5. Build configuration

Native wiring is added automatically when your bytecode references com.codename1.car. Apple grants CarPlay entitlements — and Android Auto surfaces categories — per app category, so declare the categories you ship with these build hints:

CategoryiOS hintAndroid hintNotes

Audio

ios.carplay.audio=true

(media browser service)

Default CarPlay entitlement when none specified

Communication

ios.carplay.messaging=true

android.androidAuto.messaging=true

Read-aloud / voice reply

Navigation

ios.carplay.navigation=true

android.androidAuto.navigation=true

Adds the drawable map surface + MAP_TEMPLATES permission

Point of interest

ios.carplay.poi=true

android.androidAuto.poi=true (default)

Browse/POI list apps

Optional: android.carAppVersion pins the androidx.car.app version; android.androidAuto.minCarApiLevel sets the minimum car API level.

CarPlay and the restricted Android Auto categories require approval from Apple/Google before they will run outside the simulator/Desktop Head Unit. The build wiring is automatic, but shipping to real cars still needs the relevant CarPlay entitlement on your Apple App ID (request it at developer.apple.com) and acceptance into the Android Auto programme.

47.6. Debugging in the simulator

The fastest way to build and debug an in-car experience is the Codename One simulator. Run your app as usual, then use the Car menu:

  • Car > Connect CarPlay or Car > Connect Android Auto opens a simulated head-unit window that renders your registered CarApplication in the matching style. Click rows, grid items and actions to drive navigation (pushScreen/popScreen) exactly as a real head unit would; the Back button pops the stack.

  • Car > Disconnect (or closing the window) ends the session and fires onCarDisconnected().

This needs no iOS/Android toolchain, no entitlement and no head unit — it exercises the same portable template tree and the same CarActionListener callbacks (on the EDT) that ship to the device, so you can iterate on your screens in seconds. If you haven’t called Car.setApplication(…​) the menu tells you so. The simulated head unit approximates the two platform styles and the driver-distraction row caps; it’s a debugging aid, not a pixel-accurate emulation.

Simulated CarPlay head unit rendering a CarListTemplate

Selecting a browsable row pushes the next screen, just as it would in the car — here a CarGridTemplate, reachable via the Back button:

Simulated CarPlay head unit rendering a CarGridTemplate

47.7. Testing on the real platforms

  • iOS: run the app in the iOS Simulator, then I/O > External Displays > CarPlay to open the CarPlay window.

  • Android: install the Desktop Head Unit (DHU) from the Android SDK and connect a device/emulator running Android Auto.

A complete runnable example lives in Samples/samples/CarSample.

47.8. Limitations

  • The navigation map surface (CarSurfaceCallback) is scaffolded; live map drawing onto the head-unit surface is a work in progress — the navigation template’s controls, header and ETA strip are wired.

  • Android Auto media browsing is driven by the standard media session; CarNowPlayingTemplate routes to it.

  • You can’t show arbitrary Codename One forms on the head unit — use the template catalogue above.

48. Motion Sensors

The com.codename1.sensors package exposes the device motion hardware through a single entry point, MotionSensorManager, and layers high-level gesture detection on top of the raw sensor stream. The raw sensors are the accelerometer, the gyroscope and the magnetometer. From those readings the framework derives the gravity vector, the linear acceleration and the device orientation, and it recognizes a set of common gestures: shake, flip, tilt, pick up and free fall.

Gesture detection lives in the core rather than in each platform port. Any platform that reports an accelerometer therefore gets the full gesture set, with no per-device work.

48.1. Reading a sensor

MotionSensorManager.getInstance() always returns a manager. On a device without motion hardware every sensor reports as unsupported, so application code can call the API without a null check on the manager itself. getSensor(int) returns null when the requested sensor isn’t available on the current device:

class SensorForm extends Form {
    private MotionSensor accelerometer;
    private MotionSensorListener listener;

    protected void initComponent() {
        super.initComponent();
        MotionSensorManager m = MotionSensorManager.getInstance();
        accelerometer = m.getSensor(MotionSensorManager.TYPE_ACCELEROMETER);
        if (accelerometer != null) {
            listener = new MotionSensorListener() {
                public void motionReceived(MotionEvent evt) {
                    // x, y and z are in meters per second squared
                    label.setText(evt.getX() + ", " + evt.getY() + ", " + evt.getZ());
                    label.getParent().revalidate();
                }
            };
            accelerometer.addListener(listener);
        }
    }
}

Callbacks arrive on the EDT, so the listener can update the UI directly. Sampling is reference counted: the hardware sensor draws power only while at least one listener is registered. Remove the listener once the screen is no longer visible, from the form’s deinitialize(), so the sensor powers down:

// in the same form's deinitialize(), before super.deinitialize()
// getSensor returns null on a device without the hardware
if (accelerometer != null) {
    accelerometer.removeListener(listener);
}

48.2. Sensor types

Each sensor is identified by a TYPE_* constant on MotionSensorManager. The three hardware sensors map directly to the device. The gravity, linear acceleration and orientation values are derived in the core from the accelerometer (and the magnetometer, for the compass azimuth) when the platform doesn’t report them on its own.

ConstantReading

TYPE_ACCELEROMETER

Acceleration including gravity, in meters per second squared.

TYPE_LINEAR_ACCELERATION

Acceleration with gravity removed, in meters per second squared.

TYPE_GRAVITY

The gravity vector on its own, in meters per second squared.

TYPE_GYROSCOPE

Rate of rotation around each axis, in radians per second.

TYPE_MAGNETOMETER

The ambient magnetic field, in microtesla.

TYPE_ORIENTATION

Azimuth, pitch and roll, in radians.

Readings use a single convention across every port: the x-axis points to the right, the y-axis points up, and the z-axis points out of the screen toward the user, with the device held upright in portrait. At rest, face up, the accelerometer reports about +9.81 on the z-axis.

48.3. Detecting gestures

Register a GestureListener for one of the GestureEvent.TYPE_* gestures. The accelerometer powers on for the duration that a gesture listener stays registered:

private GestureListener shakeListener;

void listenForShake() {
    shakeListener = new GestureListener() {
        public void gestureDetected(GestureEvent evt) {
            Dialog.show("Shaken", "You shook the device", "OK", null);
        }
    };
    MotionSensorManager.getInstance()
            .addGestureListener(GestureEvent.TYPE_SHAKE, shakeListener);
}

void stopListeningForShake() {
    // the accelerometer stays powered while any gesture listener is registered
    MotionSensorManager.getInstance()
            .removeGestureListener(GestureEvent.TYPE_SHAKE, shakeListener);
}

The recognized gestures are:

  • TYPE_SHAKE — the device was shaken back and forth.

  • TYPE_FLIP_FACE_DOWN and TYPE_FLIP_FACE_UP — the device was turned screen down or screen up.

  • TYPE_TILT_LEFT, TYPE_TILT_RIGHT, TYPE_TILT_FORWARD and TYPE_TILT_BACKWARD — the device crossed a tilt angle on the roll or pitch axis.

  • TYPE_PICK_UP — the device was lifted from a resting position.

  • TYPE_FREE_FALL — the device is falling (near weightlessness was detected).

The detection thresholds suit most apps as shipped, and you can tune them when a gesture needs to be more or less sensitive:

MotionSensorManager m = MotionSensorManager.getInstance();
m.setShakeThreshold(15.0);     // require a more vigorous shake
m.setTiltThreshold(0.5);       // tilt angle in radians
m.setSamplingInterval(33);     // sample at about 30Hz for snappier gestures

A smaller sampling interval makes gestures more responsive at a higher battery cost. The value is clamped to the range 10ms through 1000ms.

48.4. Device orientation

TYPE_ORIENTATION is a derived sensor. Its event carries the azimuth, pitch and roll in radians, available through getAzimuth(), getPitch() and getRoll() on MotionEvent. Pitch and roll come from the gravity vector and need only the accelerometer. The azimuth is a compass heading and needs the magnetometer; on a device without one the azimuth stays at zero.

48.5. Platform notes

  • Android — backed by android.hardware.SensorManager. Android already reports its readings in the units and axis convention this API documents, so no conversion takes place.

  • iOS — backed by CoreMotion (CMMotionManager). Add the build hint ios.NSMotionUsageDescription with a short sentence describing why the app reads motion data; iOS denies access to the motion hardware without it. The iOS Simulator doesn’t expose an accelerometer, so the sensors report as unsupported there. Test motion features on a real device.

  • Other ports — a port that doesn’t implement the sensors returns an unsupported manager, so the API stays safe to call everywhere.

48.6. Testing in the simulator

The simulator can stand in for real hardware. Open Simulate > Motion / Gesture Simulation to show a small control window. The two sliders set the simulated device orientation, which the accelerometer reflects, and the buttons inject the motion needed to fire the shake, flip, pick up and free fall gestures. This lets you wire up and debug a gesture handler on the desktop before deploying to a device.

49. Near-Field Communication (NFC)

Codename One ships an NFC API under com.codename1.nfc that covers the three things most apps need: read and write NDEF tags, exchange APDUs with ISO 7816 / MIFARE / FeliCa smart cards, and act as a host-emulated card for a nearby reader/terminal.

A single entry point exposes the platform NFC controller, a typed enum reports the technologies the discovered tag supports, and a typed error enum lets callers react to failures without string matching.

49.1. Quick start: Read an NDEF URI

Nfc nfc = Nfc.getInstance();
if (!nfc.canRead()) {
    // no NFC hardware, or it is disabled in system settings
    return;
}
nfc.readNdef(new NfcReadOptions()
        .setNdefOnly(true)
        .setAlertMessage("Hold near the poster"))
   .onResult((NdefMessage msg, Throwable err) -> {
        if (err != null) {
            return;
        }
        String url = msg.getFirstRecord().getUriPayload();
        Display.getInstance().execute(url);
   });

setNdefOnly(true) picks the fastest iOS Core NFC session (NFCNDEFReaderSession) and matches the most common tag layout. For mixed payloads use readTag(…​) and inspect tag.getTypes().

49.2. Quick start: Write an NDEF URI

NDEF records carry one of the well-known types (T, U, Sp), a MIME media type, an absolute URI, or an external domain:type. Use the factory methods on NdefRecord rather than constructing raw byte arrays:

FactoryUse for

createUri(String)

URI records — launches the associated app on tap

createText(String lang, String text)

Human-readable text

createMime(String mimeType, byte[] payload)

Binary payloads (vCard, JSON, …​)

createExternal(String domain, String type, byte[] payload)

Custom vendor types

createApplicationRecord(String pkg)

Android Application Record (AAR)

49.3. Tag-technology APIs

Beyond NDEF, the discovered Tag exposes accessors for the underlying technologies:

nfc.readTag(new NfcReadOptions()
        .setTechFilter(TagType.ISO_DEP)
        .setIsoSelectAids(myAid))
   .onResult((Tag tag, Throwable err) -> {
        if (err != null) return;
        IsoDep iso = tag.getIsoDep();
        if (iso == null) return;
        iso.transceive(myCommandApdu).onResult((byte[] resp, Throwable e) -> {
            if (ApduResponse.isSuccess(resp)) {
                byte[] body = ApduResponse.body(resp);
                // application-specific parsing
            }
        });
   });

Each accessor returns null when the tag doesn’t advertise the corresponding technology, so always null-check before calling.

On iOS the AIDs handed to setIsoSelectAids must also be listed in Info.plist under com.apple.developer.nfc.readersession.iso7816.select-identifiers. Core NFC only discovers an application the app declared up front, and the builders can’t read a runtime argument, so inject that key with ios.plistInject too. The nfc.hce.iso7816.select-identifiers entitlement the builders do set is the host-card-emulation key, which is a different thing.
AccessorAndroidiOSNotes

getIsoDep()

yes

yes

ISO 7816 — EMV, ePassport, smart cards

getMifareClassic()

yes

 — 

Apple doesn’t expose MIFARE Classic on iOS

getMifareUltralight()

yes

yes

NTAG21x, Ultralight C

getNfcA()

yes

yes (limited)

Raw ISO 14443-3A

getNfcB()

yes

 — 

Raw ISO 14443-3B (Android-only)

getNfcF()

yes

yes

FeliCa (Suica / PASMO / ICOCA)

getNfcV()

yes

 — 

ISO 15693 (Android-only)

For FeliCa on iOS, set the system codes in NfcReadOptions:

NfcReadOptions options = new NfcReadOptions()
    .setTechFilter(TagType.NFC_F)
    .setFelicaSystemCodes("0003", "8008");
nfc.readTag(options);

The builders can’t derive that from the call — the codes are a runtime argument they never see. iOS wants them in Info.plist, so inject the key at build time:

codename1.arg.ios.plistInject=<key>com.apple.developer.nfc.readersession.felica.systemcodes</key><array><string>0003</string><string>8008</string></array>

Seeing com.codename1.nfc on the classpath gets you CoreNFC, a default NFCReaderUsageDescription and the com.apple.developer.nfc.readersession.formats entitlement automatically. These system codes are the one part you supply.

49.4. Quick start: Host card emulation

Host Card Emulation (HCE) lets the device pretend to be a contactless smart card. A nearby reader (Android phone, payment terminal, access control gate) sends ISO 7816 APDUs and your app responds. Subclass HostCardEmulationService and register the instance:

class MyService extends HostCardEmulationService {
    @Override
    public String[] getAids() {
        return new String[] { "F0010203040506" };
    }
    @Override
    public byte[] processCommand(byte[] apdu) {
        if (apdu.length > 1 && apdu[1] == (byte) 0xA4) {
            // SELECT -- terminal has just routed an APDU to our AID
            return ApduResponse.withStatus(
                    new byte[] { 'O', 'K' },
                    ApduResponse.swSuccess());
        }
        return ApduResponse.swInsNotSupported();
    }
}

Nfc.getInstance().registerHostCardEmulationService(new MyService());

Set the AIDs as a build hint so they appear in the platform routing tables:

android.hceAids=F0010203040506
android.hceCategory=other

The Maven plugin and BuildDaemon generate apduservice.xml on Android, register the CodenameOneHostApduService in the manifest, and inject the iOS HCE entitlement so the same code path works on both platforms.

iOS HCE (CardSession) requires iOS 17.4+ and, as of 2026, is EU-only. The Codename One plugin still injects the entitlement when you reference the class, but canHostEmulate() will report false on non-EU devices.

49.5. Permissions and build hints

The Codename One build pipeline automatically detects NFC usage. Apps that never touch com.codename1.nfc see no manifest / plist change.

ReferenceAndroid injectediOS injected

com.codename1.nfc.*

android.permission.NFC + <uses-feature android:name="android.hardware.nfc">

NFCReaderUsageDescription, com.apple.developer.nfc.readersession.formats=TAG\nNDEF, CoreNFC.framework

com.codename1.nfc.HostCardEmulationService

BIND_NFC_SERVICE permission, android.hardware.nfc.hce feature, <service> entry, generated res/xml/apduservice.xml

com.apple.developer.nfc.hce and …​hce.iso7816.select-identifiers entitlements

Override any of the defaults with the matching build hint:

Build hintDefaultNotes

ios.NFCReaderUsageDescription

"Hold near an NFC tag to continue"

Localise this — Apple rejects builds that ship the default copy

ios.entitlements.com.apple.developer.nfc.readersession.formats

TAG\nNDEF

Multi-line entitlement value

android.hceAids

(none)

Comma-separated; required for HCE on Android

android.hceCategory

other

payment for EMV-conformant apps

android.hceRequireUnlock

false

Force lock screen unlock before APDU routing

ios.hceAids

falls back to android.hceAids

iOS HCE AID list

49.6. Simulator support

The JavaSE simulator implements Nfc via a virtual tag you can edit and tap from the Simulate → NFC submenu:

Menu itemEffect

Hardware Available / NFC Enabled / HCE Available

Toggle what isSupported() / canRead() / canHostEmulate() return

Next read outcome

Pick DISCOVER_TAG, USER_CANCELED, TAG_LOST, TIMEOUT, or READ_ONLY for the next readTag() call

Tap virtual tag

Fire the configured outcome on any pending read or registered listener

Set virtual tag URI…​

Replace the virtual tag’s NDEF payload with a URI record

Set virtual tag text…​

Replace the virtual tag’s NDEF payload with a text record

Tag is read-only

Lock the virtual tag so the next write fails with READ_ONLY

Send APDU to HCE service…​

Hex-entry dialog that dispatches the bytes to your registered HostCardEmulationService and shows the response

Deactivate HCE field

Fires onDeactivated(DEACTIVATION_LINK_LOSS) on the registered service

All toggles persist between simulator runs (under NfcSim.* user preferences) so you can drive tests deterministically.

49.7. Platform behaviour

PlatformNDEF readNDEF writeTag techHCENotes

Android

yes

yes

full set

yes

NfcAdapter.enableReaderMode (API 19+); HostApduService (API 19+)

iOS

yes (iOS 11+)

yes (iOS 13+)

ISO 7816, FeliCa, MIFARE Ultralight

iOS 17.4+ EU-only

MIFARE Classic and NFC-B/V not exposed by Apple

JavaSE simulator

yes (virtual tag)

yes (virtual tag)

configurable

yes (synthetic APDUs)

Drive everything from the Simulate → NFC menu

Desktop deploy / JavaScript

no

no

no

no

Fallback base class — every method completes with NfcError.NOT_AVAILABLE

Always gate user-facing affordances on canRead() (for tag operations) or canHostEmulate() (for HCE). isSupported() only reports whether the hardware is present.

50. Bluetooth

Codename One ships a first-class Bluetooth API under com.codename1.bluetooth that covers both Bluetooth Low Energy roles and classic Bluetooth: scan for and connect to BLE peripherals (the GATT client), act as a BLE peripheral (a local GATT server plus advertising), stream bytes over L2CAP connection-oriented channels, and exchange RFCOMM streams with classic serial devices.

Bluetooth.getInstance() is the single entry point and never returns null — ports without Bluetooth return a fallback whose operations fail fast with BluetoothError.NOT_SUPPORTED, so calling code needs no platform-specific if. Every callback of the API — AsyncResource results, scan sightings, connection events, notifications, adapter state changes — is delivered on the EDT; the only exceptions are the blocking RFCOMM/L2CAP streams, which must be consumed off the EDT.

CapabilityAndroidiOS / macOSSimulator (mock)Simulator (native backend)JavaScript

BLE central (scan, GATT client)

yes

yes

yes

yes

browser chooser only

BLE peripheral (GATT server + advertising)

yes

yes (not tvOS/watchOS)

yes

 — 

 — 

L2CAP channels

yes (Android 10+)

yes (iOS 11+)

yes

 — 

 — 

Classic RFCOMM

yes

 — 

yes

 — 

 — 

Bonding

yes

OS-managed

yes

 — 

 — 

On the JavaScript port, Web Bluetooth limits the central role to the browser’s device chooser (Chrome-family browsers over HTTPS): startScan(…​) surfaces the chooser built from the scan filters and delivers the single user-picked device as one ScanResult. No further sightings ever arrive, the RSSI is unknown, the MTU is fixed at 512 and peripheral mode, classic Bluetooth and L2CAP are unavailable.

Always branch through the capability queries — isLeSupported(), isPeripheralModeSupported(), isL2capSupported(), isClassicSupported() — rather than through platform detection.

50.1. Quick start: Scan and connect

Bluetooth bt = Bluetooth.getInstance();
if (!bt.isLeSupported()) {
    // hide the feature on ports without BLE
    return;
}
bt.requestPermissions(BluetoothPermission.SCAN, BluetoothPermission.CONNECT)
        .onResult((granted, permissionErr) -> {
    if (permissionErr != null || !granted) {
        return;
    }
    ScanSettings settings = new ScanSettings()
            .addFilter(new ScanFilter()
                    .setServiceUuid(BluetoothUuid.fromShort(0x180D))
                    .setNamePrefix("Polar"));
    BleScan[] scan = new BleScan[1];
    scan[0] = bt.getLE().startScan(settings, sighting -> {
        scan[0].stop();
        sighting.getPeripheral().connect()
                .onResult((peripheral, connectErr) -> {
            if (connectErr != null) {
                return;
            }
            peripheral.discoverServices()
                    .onResult((services, discoverErr) -> {
                // the GATT database is ready -- see the
                // following sections
            });
        });
    });
});

ScanFilter criteria on one filter are AND-combined; multiple filters added to the same ScanSettings are OR-combined, and a scan without filters reports every advertising device. Any number of scans may run concurrently — each BleScan handle only sees advertisements matching its own filters, and the platform scan stops when the last handle stops. On iOS a service-UUID filter is also what keeps scans alive in the background, so prefer filtered scans.

Peripheral addresses returned by BluetoothDevice.getAddress() are stable per app install — the MAC address on Android and desktop, the per-app CoreBluetooth identifier on iOS — and safe to persist for later reconnection through BluetoothLE.getPeripheral(String). They’re not portable across devices or platforms, so never treat them as the peripheral’s real hardware address.

getAdapterState() reports where the local radio stands (POWERED_ON, POWERED_OFF, UNAUTHORIZED, …​) and addAdapterStateListener(…​) fires on the EDT whenever it changes — the natural place to pause scans and gray out UI. requestEnable() shows the Android system dialog asking the user to turn the adapter on; on iOS there is no programmatic enable flow, so it resolves false and the app should direct the user to the Settings app instead.

Connection lifecycle is equally explicit. connect(ConnectionOptions) accepts a timeout (iOS connect requests never time out on their own, so set one) and an autoConnect flag that asks the platform to re-establish the link whenever the peripheral comes back into range. addConnectionListener(…​) reports every transition including link loss, and every failure across the API surfaces as a BluetoothException whose getError() returns a typed BluetoothError constant — no string matching required.

50.2. Reading and writing characteristics

discoverServices() caches the peripheral’s GATT database; from there getService(…​), getCharacteristic(…​) and the convenience accessors on GattCharacteristic cover the day-to-day operations:

GattCharacteristic measurement = peripheral.getCharacteristic(
        BluetoothUuid.fromShort(0x180D),
        BluetoothUuid.fromShort(0x2A37));
if (measurement != null && measurement.canRead()) {
    measurement.read().onResult((data, err) -> {
        if (err == null) {
            // raw bytes, delivered on the EDT
            updateHeartRateLabel(data[1] & 0xFF);
        }
    });
}

GattCharacteristic controlPoint = peripheral.getCharacteristic(
        BluetoothUuid.fromShort(0x180D),
        BluetoothUuid.fromShort(0x2A39));
if (controlPoint != null && controlPoint.canWrite()) {
    controlPoint.write(new byte[] {1})
            .onResult((acknowledged, err) -> {
        // resolves once the peripheral acknowledged the write
    });
}

All GATT operations return independent AsyncResource handles and may be issued concurrently: an internal per-peripheral queue serializes them toward the platform stack (which allows only one in-flight request per connection) and applies a safety timeout, so a lost platform callback can never wedge the queue. Operations on different peripherals run fully concurrently. writeWithoutResponse(…​) resolves once the value is queued to the controller — there is no remote acknowledgement.

A default BLE connection carries 23 bytes per attribute operation; requestMtu(…​) negotiates a larger value and resolves with what was granted (on iOS the OS negotiates by itself, so the request resolves immediately with the current value — check getMtu() before chunking data). requestConnectionPriority(…​) trades latency against battery on Android and is a successful no-op on iOS. createBond() initiates pairing where the peripheral requires an encrypted link; on iOS bonding is OS-managed and triggered automatically by encrypted characteristics, so the call resolves true without user interaction.

50.3. Notifications

GattCharacteristic measurement = peripheral.getCharacteristic(
        BluetoothUuid.fromShort(0x180D),
        BluetoothUuid.fromShort(0x2A37));
GattNotificationListener listener = (characteristic, data) -> {
    // fires on the EDT for every notification while subscribed
    updateHeartRateLabel(data[1] & 0xFF);
};
measurement.subscribe(listener).onResult((armed, err) -> {
    if (err != null) {
        // the CCCD write was rejected or the link dropped
    }
});

// later, e.g. when leaving the form:
measurement.unsubscribe(listener);

The Client Characteristic Configuration descriptor (CCCD) is handled automatically: the first listener triggers the descriptor write that arms notifications (indications are used when the characteristic only supports those) and removing the last listener disarms it again. Values stream to the listeners on the EDT until unsubscribed or disconnected.

50.4. Peripheral mode

When isPeripheralModeSupported() returns true the device can serve data itself — open a local GATT server, add service definitions and advertise them:

Bluetooth bt = Bluetooth.getInstance();
if (!bt.isPeripheralModeSupported()) {
    return;
}
GattLocalCharacteristic status = new GattLocalCharacteristic(
        BluetoothUuid.fromShort(0x2A37),
        GattCharacteristic.PROPERTY_READ
                | GattCharacteristic.PROPERTY_NOTIFY,
        GattLocalCharacteristic.PERMISSION_READ)
        .setValue(new byte[] {0, 72});
GattLocalService service = new GattLocalService(
        BluetoothUuid.fromShort(0x180D))
        .addCharacteristic(status);
bt.getLE().openGattServer(new GattServerAdapter() {
    @Override
    public void centralConnected(BleCentral central) {
        // a central connected to this device
    }

    @Override
    public void characteristicWriteRequest(GattWriteRequest request) {
        // handle the written bytes, then acknowledge
        if (request.isResponseRequired()) {
            request.respond();
        }
    }
}).onResult((server, err) -> {
    if (err != null) {
        return;
    }
    server.addService(service).onResult((added, addErr) -> {
        if (addErr != null) {
            return;
        }
        bt.getLE().startAdvertising(
                new AdvertiseSettings(),
                new AdvertiseData()
                        .addServiceUuid(BluetoothUuid.fromShort(0x180D))
                        .setIncludeDeviceName(true),
                null).onResult((advertisement, advErr) -> {
            // broadcasting; keep the handle and stop() it when done
        });
        // push a new value to every subscribed central
        server.notifyValue(status, new byte[] {0, 80});
    });
});

Characteristics with a static value (setValue(…​)) are served without involving the listener; characteristics without one route every read to characteristicReadRequest(…​), which must answer via respond(…​) or reject(…​) — centrals time out unanswered requests.

Two iOS platform notes: CoreBluetooth only broadcasts the local name and service UUIDs, dropping manufacturer data, service data and TX power from advertisements (Android broadcasts everything). And iOS serves descriptors from their static values only — descriptor read/write requests never reach the listener there, so always give local descriptors a static value for cross-platform behavior.

50.5. L2CAP channels

L2CAP connection-oriented channels provide a raw bidirectional byte stream — much higher throughput than chunking data through GATT characteristics. The peripheral side opens a listener and publishes its Protocol/Service Multiplexer (PSM), typically through a GATT characteristic; the central side connects to that PSM:

if (!Bluetooth.getInstance().isL2capSupported()) {
    return;
}
peripheral.openL2capChannel(psm, true).onResult((channel, err) -> {
    if (err != null) {
        return;
    }
    // the streams block -- always consume them off the EDT
    CN.startThread(() -> {
        try {
            OutputStream out = channel.getOutputStream();
            out.write(new byte[] {1, 2, 3});
            out.flush();
            InputStream in = channel.getInputStream();
            byte[] buffer = new byte[4096];
            int size = in.read(buffer);
            // process the response...
        } catch (IOException ioErr) {
            // transport failure
        } finally {
            try {
                channel.close();
            } catch (IOException ignored) {
            }
        }
    }, "L2CAP IO").start();
});

// the peripheral side listens and publishes its PSM to centrals
Bluetooth.getInstance().getLE().openL2capServer(true)
        .onResult((server, serverErr) -> {
    if (serverErr == null) {
        int assignedPsm = server.getPsm();
        // advertise assignedPsm through a GATT characteristic,
        // then server.accept() incoming channels
    }
});

On Android an L2CAP channel establishes its own link and doesn’t require connect() first; on iOS the peripheral must be connected. The streams throw plain java.io.IOException on transport failure and, like all Bluetooth streams, block — never touch them on the EDT.

50.6. Classic Bluetooth

Classic (BR/EDR) Bluetooth covers inquiry discovery, bonding and RFCOMM stream connections — the Serial Port Profile (SPP) world of printers, scanners and industrial equipment. iOS doesn’t expose classic Bluetooth to applications, so this role is Android, desktop and simulator only; isClassicSupported() is the gate:

Bluetooth bt = Bluetooth.getInstance();
if (!bt.isClassicSupported()) {
    // classic Bluetooth is unavailable -- iOS among others
    return;
}
BluetoothClassic classic = bt.getClassic();
classic.startDiscovery(sighting -> {
    if ("SerialPrinter".equals(sighting.getDevice().getName())) {
        classic.connect(sighting.getDevice(), BluetoothUuid.SPP, true)
                .onResult((connection, err) -> {
            if (err != null) {
                return;
            }
            // consume the blocking streams off the EDT, exactly
            // like the L2CAP example
        });
    }
});

// accepting incoming SPP clients
classic.listen("Codename One SPP", BluetoothUuid.SPP, true)
        .onResult((server, err) -> {
    if (err == null) {
        server.accept().onResult((connection, acceptErr) -> {
            // one client accepted; call accept() again for the next
        });
    }
});

startDiscovery(…​) runs an inquiry scan (about 12 seconds) and returns a live ClassicDiscovery handle that resolves when the inquiry ends. getBondedDevices() lists already-paired devices without a discovery, and connect(String, BluetoothUuid, boolean) reconnects to a persisted address directly.

50.7. Permissions and build hints

The build pipeline scans the application’s bytecode and injects only what the referenced packages need — apps that never touch com.codename1.bluetooth see no manifest or plist change, and a central-only app never carries advertise permissions.

Detected usageAndroid injected (target SDK 31+)iOS injected

Any com.codename1.bluetooth reference

BLUETOOTH (capped at maxSdkVersion="30"), <uses-feature android:name="android.hardware.bluetooth_le">

NSBluetoothAlwaysUsageDescription + NSBluetoothPeripheralUsageDescription defaults (only if unset), CoreBluetooth.framework, the CN1 Bluetooth natives

Scanning (ScanSettings / BleScan / classic discovery)

BLUETOOTH_ADMIN (capped at 30), BLUETOOTH_SCAN with usesPermissionFlags="neverForLocation", ACCESS_FINE_LOCATION (capped at 30)

 — 

Connections (GATT, BlePeripheral, RFCOMM, L2CAP)

BLUETOOTH_CONNECT

 — 

Peripheral mode (com.codename1.bluetooth.le.server)

BLUETOOTH_ADMIN (capped at 30), BLUETOOTH_ADVERTISE

 — 

Classic (com.codename1.bluetooth.classic)

<uses-feature android:name="android.hardware.bluetooth">

not supported

Builds targeting an SDK below 31 receive the legacy permissions uncapped and none of the Android 12 ones. Permissions you declare yourself (via android.xpermissions) are never duplicated.

Override the defaults with the matching build hints:

Build hintDefaultNotes

android.bluetooth.neverForLocation

true

Declares that scan results never derive the user’s location, which caps ACCESS_FINE_LOCATION at API 30 and flags BLUETOOTH_SCAN accordingly

android.bluetooth.required

false

Marks the BLE hardware feature required so stores hide the app on devices without it

ios.bluetooth.background

(none)

central, peripheral or central,peripheral — merged into UIBackgroundModes as bluetooth-central / bluetooth-peripheral

ios.NSBluetoothAlwaysUsageDescription

"Communicates with nearby Bluetooth accessories."

Localise this — Apple rejects builds that ship generic privacy copy

Beacon and indoor-positioning apps are the one case where the neverForLocation default is wrong: an app that derives location from BLE sightings must set android.bluetooth.neverForLocation=false, otherwise Android 12+ filters beacon advertisements out of its scan results. With the hint set to false the location permission stays uncapped and the app keeps prompting for it on Android 12+.

50.8. Simulator support

The JavaSE simulator implements the whole API against a scriptable virtual Bluetooth stack. Simulate → Bluetooth Simulation opens a window with a tree of the simulated adapter, the virtual peripherals and the app’s own peripheral role; selecting a node opens its detail editor (hex value editors on characteristics), a toolbar stages demo devices, failure injection arms the next operation of a chosen kind (connect, read, write, discover, subscribe, scan and the rest) to fail with a chosen BluetoothError, a latency spinner delays every asynchronous completion and a live event log shows what the stack does. All settings persist between simulator runs.

The Bluetooth Simulation window with the device tree and a characteristic editor

The window’s backend selector switches the simulator between the virtual stack and a native backend that drives the host machine’s real Bluetooth radio through a bundled btleplug library (libcn1ble) loaded in-process (CoreBluetooth on macOS, BlueZ on Linux, WinRT on Windows). The native backend is central-only — real scanning, connections and GATT operations against physical devices, without leaving the simulator. The initial backend comes from the cn1.bluetooth.backend system property (default simulator); point cn1.bluetooth.libraryPath at a locally built libcn1ble to override the bundled one.

The Bluetooth Simulation event log while a scan and connect run

The Simulate → Bluetooth menu exposes the same operations as one-click hooks; every item is also callable from cross-platform code via CN.execute("bluetooth:itemN"):

HookMenu labelEffect

bluetooth:item1

Toggle Adapter

Flips the simulated adapter between POWERED_ON and POWERED_OFF

bluetooth:item2

Add Demo Peripheral

Registers the canonical demo device: AA:BB:CC:DD:EE:01 "SimulatedSensor" with service 0x180D, a read/write/notify characteristic 0x2A37 (carrying a CCCD) and a writable 0x2A39

bluetooth:item3

Push Demo Notification

Pushes a rolling one-byte notification from the demo peripheral’s 0x2A37 characteristic

bluetooth:item4

Disconnect All

Drops every live connection from the remote side

bluetooth:item5

Clear Peripherals

Removes every registered virtual peripheral

bluetooth:item6

Use Simulator Backend

Switches back to the virtual stack

bluetooth:item7

Use Native Backend

Switches to the host machine’s real radio

bluetooth:item8

(API only)

Arms the next GATT read to fail with GATT_ERROR

Everything the window does goes through the scriptable BluetoothSimulator API in com.codename1.impl.javase.bluetooth, so tests and sample apps can stage the same devices programmatically. This code runs against the JavaSE port only — it isn’t part of the cross-platform API and won’t compile in a portable code base, so keep it in your test or debug scaffolding:

BluetoothSimulator.addPeripheral(
        new VirtualPeripheral("AA:BB:CC:DD:EE:10")
                .setName("Thermometer")
                .addAdvertisedServiceUuid(BluetoothUuid.fromShort(0x1809))
                .withService(new VirtualService(BluetoothUuid.fromShort(0x1809))
                        .withCharacteristic(new VirtualCharacteristic(
                                BluetoothUuid.fromShort(0x2A1C),
                                GattCharacteristic.PROPERTY_READ
                                        | GattCharacteristic.PROPERTY_INDICATE,
                                new byte[] {0, 0, 0, 98})
                                .withDescriptor(new VirtualDescriptor(
                                        BluetoothUuid.CCCD, new byte[] {0, 0})))));
BluetoothSimulator.setLatencyMillis(50);
BluetoothSimulator.failNext("connect",
        BluetoothError.CONNECTION_FAILED, "Injected for testing");

50.8.1. Record and replay

Fixtures bridge the two backends: the window’s "Record from real hardware" button (or scripts/bluetooth/capture-fixture.sh) scans the host machine’s real radio through the native backend for a chosen duration, captures device identities, RSSI timelines, advertisement payloads and GATT databases, scrambles the trace deterministically so committed fixtures never carry real device identities, and writes it as JSON. BluetoothSimulator.loadFixture(InputStream) replays the trace into the virtual stack — devices appear at their recorded first-sighting times and their RSSI timelines replay on the stack’s scheduler, so a scan against messy real-world radio traffic becomes a reproducible test input.

50.9. Testing

Cross-platform test code drives the simulation through the hooks: CN.execute("bluetooth:item2") stages the demo peripheral, the test scans for SimulatedSensor, connects and subscribes to 0x2A37, and CN.execute("bluetooth:item3") pushes a notification to assert on — no simulator-only imports needed, so the same test class compiles for device targets and runs unchanged in the simulator’s test runner. Failure paths test the same way: CN.execute("bluetooth:item8") arms the next GATT read to fail with GATT_ERROR, letting a portable test verify the app’s error handling.

For fully deterministic unit tests the JavaSE port also allows constructing a private SimulatedBluetoothStack on a ManualScheduler, where time only advances when the test pumps it — no sleeps, no flakiness from real timers; see the Bluetooth tests under the javase Maven module for the pattern.

50.10. Platform behaviour

PlatformAddress semanticsMTUBondingNotes

Android

Real MAC address

requestMtu(…​) negotiates, default 23

createBond() prompts the user

Background scanning is throttled by the OS; use filtered scans and expect slower sightings

iOS / macOS

Per-app CoreBluetooth identifier UUID

Negotiated by the OS; requestMtu(…​) resolves with the current value

OS-managed, triggered by encrypted characteristics; getBondedPeripherals() is empty

Background operation needs ios.bluetooth.background plus service-UUID scan filters; advertisements carry only name and service UUIDs

Simulator (mock)

Synthetic (AA:BB:CC:DD:EE:01)

Simulated negotiation

Simulated

Timestamps come from a monotonic counter — treat them as ordered, not as wall-clock times

Simulator (native backend)

Host-OS device identifier

Real negotiation

Not supported

Central role only; requires the bundled libcn1ble library and OS Bluetooth permission for the terminal/IDE

JavaScript

Per-origin Web Bluetooth identifier

Fixed at 512

Browser-managed

One user-picked device per chooser; startScan requires a user gesture (call it from a button handler); RSSI unavailable

Persist addresses only for same-install reconnection, branch on the capability queries rather than on Display.getPlatformName(), and keep every byte-stream consumer off the EDT — those three rules keep one code base working across every column of the capability matrix.

51. Nearby Devices

Codename One answers three questions about the surrounding devices, under com.codename1.nearby: how far away one is and in which direction (com.codename1.nearby.ranging), which one is yours (com.codename1.nearby.companion), and how to send it something (com.codename1.nearby.transport).

They’re three packages rather than one because referencing a package is the whole opt-in. The build server decides what native machinery an app gets by scanning bytecode for these prefixes, so an app that only wants to know how far away its keyring tag is pays for ranging alone — no Play Services dependency, no local network prompt, no companion permissions. Referencing com.codename1.nearby itself costs nothing; it holds only the shared value types.

CapabilityiOSAndroidSimulator and desktopJavaScript

Precision ranging, peer to peer

yes (U1 chip, iPhone 11 and later)

yes (UWB hardware)

simulated

simulated

Ranging an accessory

yes (Nearby Interaction Accessory Protocol)

yes (join the session it names)

simulated

simulated

Direction as well as distance

where the hardware provides it

where the hardware provides it

simulated

simulated

Companion association

yes (iOS 18 and later)

yes (Android 8 and later)

simulated

simulated

Presence notifications

 — 

yes (Android 12 and later)

simulated

simulated

Device-to-device transport

Apple devices only

Android devices only

simulated loopback

simulated loopback

Connection authentication token

 — 

yes

 — 

 — 

Branch on the capability queries — Ranging.isSupported(), Ranging.getCapabilities(), CompanionDevices.isSupported(), NearbyTransport.isSupported() — rather than on platform detection. Ranging in particular is absent on plenty of current phones, so treat it as an enhancement to a feature that also works without it rather than as the feature itself.

Every callback in this family arrives on the EDT.

51.1. Three Things Worth Knowing Before You Design Around This

The transport doesn’t cross ecosystems. Underneath are Google’s Nearby Connections on Android and Apple’s MultipeerConnectivity on iOS, which share no wire protocol. An iPhone and an Android phone will never discover each other here, however the app is written. Nothing in the API hides that, because an API that looked portable and never found the peer would be worse than an honest limitation. When both ends aren’t the same platform, two things that do work across the divide are already in the framework: com.codename1.bluetooth.le.L2capChannel for a raw byte stream over BLE, and com.codename1.io.bonjour plus ordinary sockets when both devices share a Wi-Fi network.

Which nearby transport suits which pair of devices

Ranging needs com.codename1.bluetooth, or something like it. Both platforms require the two devices to swap a token over a channel they already share before any radio ranging can begin. A GATT characteristic is the usual channel. The two APIs are designed to be used together.

Background ranging is opt-in and needs Apple’s permission. On iOS it requires the com.apple.developer.nearby-interaction entitlement, which has to be enabled on the App ID before it will sign. Codename One never injects it on its own, because an entitlement the App ID doesn’t carry fails codesigning with an error naming the entitlement and not the reason it appeared. Set ios.nearby.background=true once the capability is enabled, and note that RangingCapabilities.isBackgroundRangingSupported() reports false until then.

51.2. Ranging: How Far, And Which Way

Ultra-wideband measures distance by timing a radio round trip, which is worth about ten centimeters. That’s a different kind of answer from a Bluetooth signal-strength estimate, which is worth a few meters on a good day and swings when someone puts a hand over the phone.

A session is prepared, then started. There’s no honest one-call form, because the token exchange has to happen between the two:

if (!Ranging.isSupported()) {
    return;                       // no ultra-wideband radio on this device
}
Ranging.prepareSession(RangingRole.CONTROLLER).onResult((session, err) -> {
    if (err != null) {
        return;
    }
    // 1. publish our token however the two apps already talk
    characteristic.write(session.getLocalToken().toByteArray());

    // 2. when theirs arrives, start measuring
    session.addRangingListener(new RangingAdapter() {
        public void updated(RangingUpdate u) {
            if (u.hasDistance()) {
                label.setText(Math.round(u.getDistance(RangingUnit.CENTIMETERS)) + " cm");
            }
            if (u.hasDirection()) {
                arrow.setAngle(u.getAzimuth());
            }
        }
    });
    session.start(RangingToken.fromByteArray(theirToken));
});

One session ranges one peer. That’s a hard limit of Apple’s NINearbyPeerConfiguration rather than a simplification, so an app tracking several peers prepares several sessions.

Pick a role even though iOS ignores it: Android needs exactly one controller, and choosing costs nothing on the other side.

Every field of a RangingUpdate except the timestamp is optional, and they drop out independently. A peer directly behind the phone commonly reports a distance with no direction, and a peer at the edge of range reports neither — so guard each read with its has method rather than assuming a sentinel. There’s no zero-argument getDistance(): meters read as feet is the accident that convention exists to prevent.

Azimuth is degrees in the range -180 to 180, zero straight ahead and positive to the right; elevation is -90 to 90, positive above the device. Android reports both angles natively. iOS reports a unit direction vector instead and the port derives the angles from it, so the same code reads the same on both; getDirectionVector() still hands back the untouched vector where there is one.

A peer that walks away produces peerRemoved and the session stays alive, ready to resume if it comes back — gray the UI out rather than tearing it down. A session that dies for good produces invalidated and can’t be restarted.

51.3. Ranging An Accessory

A third-party ultra-wideband tag isn’t a phone, and the two platforms disagree about what talking to one means.

On iOS there is a defined handshake. The accessory publishes a blob of configuration data over its own channel, and the session answers with bytes that have to travel back before the accessory begins ranging:

Ranging.prepareSession(RangingRole.CONTROLLER).onResult((session, err) -> {
    if (err != null) {
        return;
    }
    session.addRangingListener(listener);
    session.startAccessory(configurationFromTheAccessory)
            .onResult((shareable, failure) -> {
                if (failure == null) {
                    characteristic.write(shareable);   // forward it back
                }
            });
});

Android has no equivalent protocol. There, an accessory simply names the channel and session to join, so build a token from what it published and call start:

RangingToken tag = RangingToken.forUwbAddress(address, channel, preambleIndex,
        sessionId, sessionKey);
session.start(tag);

startAccessory fails with NearbyError.NOT_SUPPORTED on Android, and a token built by forUwbAddress is rejected on iOS. A token is opaque and never portable between the two platforms; RangingToken.fromByteArray says so rather than handing garbage to a native call.

51.4. Companion Devices: Which One Is Yours

Associating isn’t pairing. It’s the app telling the operating system that a particular accessory belongs to it, through a chooser the OS draws and the user picks from, and getting privileges back that an ordinary Bluetooth scan doesn’t carry: the OS watches for the device instead of the app, scanning stops needing location permission on Android, and the user sees one honest prompt naming one device.

AssociationRequest request = new AssociationRequest.Builder()
        .addFilter(DeviceFilter.bleService("180D"))
        .build();
CompanionDevices.associate(request).onResult((device, err) -> {
    if (err == null) {
        Preferences.set("sensor", device.getId());
        CompanionDevices.startObservingPresence(device.getId());
    }
});

An association outlives the app: it survives restarts and reboots, and ends only when the app drops it, the user revokes it in system settings, or the app is uninstalled. Persist CompanionDevice.getId() and look the device up again on the next launch instead of asking the user to pick it twice. CompanionDevice.getAddress() is the same handle BluetoothLE.getPeripheral(String) takes, which is what makes an association useful rather than decorative.

Ask for CompanionProfile.GENERIC unless the device is a watch, a head-mounted display or a computer. A profile is a request for elevated privileges as much as a description, and the specific ones cost the user a stronger prompt. The build scanner can’t see which profile a request asks for, because it arrives as an enum constant, so name it yourself: set android.nearby.watchProfile, android.nearby.computerProfile or android.nearby.glassesProfile to true for whichever of CompanionProfile.WATCH, COMPUTER and GLASSES you use. Each declares that profile’s own permission, and without it Android rejects the association before the chooser opens — which looks to the user like nothing happened.

Profiles arrived at different Android versions: WATCH at 12, COMPUTER at 13, GLASSES at 14. Asking for one the running device doesn’t have fails with NearbyError.NOT_SUPPORTED rather than associating without it. A profile is a request for elevated privileges, and an association that lacks them without saying so is worse than one that didn’t happen. Fall back to CompanionProfile.GENERIC yourself if that’s what you want.

Two platform differences to design around. Presence notifications are Android only: AccessorySetupKit reports an accessory being added to or removed from the app’s set, which isn’t the same event as it coming into range, so startObservingPresence answers false on iOS and an app that needs live proximity there should scan with com.codename1.bluetooth. And AccessorySetupKit only ever discovers Bluetooth services an app declared up front, so set ios.nearby.accessoryServices to a comma-separated list of the service UUIDs your accessories advertise — without it the picker finds nothing on iOS, and the build log says so.

Register the presence listener from your app’s init() rather than from a form. Android may start the process to deliver a sighting and nothing else, with no form on screen, and a listener that a form registers doesn’t exist yet at that point. An event that arrives before any listener is registered is held and replayed to the first one that registers, so a wake-up isn’t lost, but only the 64 most recent are kept.

Presence doesn’t run your code the moment a device comes into range. Android can start the process for the companion service alone, and Codename One doesn’t run an application’s init() there — a form has nowhere to live in a service. Your listener hears about the sighting, in order, when the app next initializes. Treat presence as a record of what happened while the app was away rather than as a background execution mechanism; for work that must happen without the app, use the background features in the notifications chapter.

51.5. Transport: Sending Something

NearbyTransport.addTransportListener(new TransportAdapter() {
    public void endpointFound(Endpoint e) {
        NearbyTransport.requestConnection(e, "Shai's phone");
    }
    public void connectionRequested(IncomingConnection r) {
        // show r.getAuthenticationToken() on both screens first
        r.accept();
    }
    public void connected(Endpoint e) {
        NearbyTransport.send(e, Payload.fromBytes(data));
    }
    public void payloadReceived(Endpoint e, Payload p) {
        process(p.getBytes());
    }
});
NearbyTransport.startAdvertising("chat", "Shai's phone", TransportStrategy.CLUSTER);
NearbyTransport.startDiscovery("chat", TransportStrategy.CLUSTER);

getAuthenticationToken() is a short string both devices derive from the connection’s own key exchange, so a device relaying between them can’t make both ends show the same value. Showing it on both screens and asking whether they match is what makes the pairing trustworthy; skipping that step is a choice to trust whoever answered first.

It’s empty on iOS. MultipeerConnectivity exposes nothing to derive one from, and a token computed from the service name and the display names would be one a relay can reproduce at both ends — a check that looks like a defense and isn’t. An iOS app that needs to know who it’s talking to has to establish that itself, over a channel the relay doesn’t control.

Answer every connectionRequested. You don’t have to answer inside the callback — showing the token and waiting for the user is the whole point, and the request stays live until you call accept() or reject() — but a request that’s never answered at all holds radio resources open on both sides until the far end times out. If nothing is listening when a request arrives, it’s rejected for you, so the far end learns immediately instead of waiting.

The strategy you pass to startAdvertising and startDiscovery is a limit, not a hint. Under POINT_TO_POINT a second connection is refused with NearbyError.BUSY on either side, and under STAR the discovering side holds one connection while the advertising side accepts many. CLUSTER is the only one with no limit. Disconnect before connecting elsewhere.

On iOS, list your service ids at build time. The service id becomes a Bonjour service type there, and iOS browses only the types an app declared in its Info.plist — a type that isn’t declared produces no peers and no error. The build can’t see the strings you pass to startAdvertising, so name them in ios.nearby.serviceType as a comma-separated list. Miss one and the call fails with a message telling you which id to add, which beats an app that finds nothing and says nothing.

The platform restricts the type to fifteen characters of lowercase letters, digits and hyphens, so a reverse-DNS string that’s legal on Android is folded to fit: com.example.chat becomes something like com-exampl-jd3q. The last four characters are derived from the whole id, because the fold alone is lossy — com.example.chat and com.example.charts both reduce to com-example-cha, and without the suffix two unrelated apps would discover each other’s peers. Case doesn’t split a service: Chat and chat are the same id and get the same type. The build log names every type it declared.

Byte payloads are capped at NearbyTransport.getMaxPayloadSize(), a few kilobytes on both platforms; anything larger goes as a file payload, which streams and reports progress. Call NearbyTransport.stop() when the feature’s UI closes — both platforms keep the radios busy until something says stop.

A terminal PayloadStatus.SUCCESS means the bytes reached the peer, not that they were handed to the radio. Watch for it rather than treating a resolved send() as delivery: the resource resolves when the platform accepts the payload, which is earlier. If the peer disappears between the two you get FAILURE, so every send reaches one terminal status or the other.

51.6. Developing Without Hardware

The simulator, the desktop ports and the JavaScript port carry a working implementation rather than a stub, and report NearbyAvailability.LOCAL_ONLY so an app can tell the developer its peers are not real. Almost none of a ranging feature is about radios — laying out the screen, animating an arrow, deciding what to show while the direction drops out, handling the peer walking away — and a port that answered NOT_SUPPORTED would make every line of it testable only on a pair of phones.

Two things it does that a mock wouldn’t. It never completes inline, because code written against an implementation that answers instantly races the moment it meets one that doesn’t. And its peers move, along a bounded random walk, because a constant 1.5 m would let an app ship with a distance label that flickers unreadably against real hardware.

What it won’t do behind your back is drop a peer or suspend a session at random. Those are real events an app must handle, but a simulation that fired them unpredictably would make every test using it flaky, so they’re controls the simulator drives instead.

51.7. Build Hints

HintDefaultWhat it does

ios.nearby.serviceType

derived from the package name

Comma-separated list of the service ids the app passes to startAdvertising. Each is folded to a Bonjour service type and declared; the runtime refuses an id that isn’t on the list.

ios.nearby.accessoryServices

unset

Comma-separated Bluetooth service UUIDs the association picker may discover. Required for the picker to find anything on iOS.

ios.nearby.background

false

Requests the com.apple.developer.nearby-interaction entitlement and the matching background mode. Enable the capability on the App ID first.

android.nearby.watchProfile

false

Declares the watch companion profile permission, for an app that associates with CompanionProfile.WATCH.

android.nearby.computerProfile

false

The same for CompanionProfile.COMPUTER, which Android honours from API 33.

android.nearby.glassesProfile

false

The same for CompanionProfile.GLASSES, which Android honours from API 34.

Everything else is automatic. Referencing a package links its frameworks, injects its privacy strings and adds its permissions; referencing none of them changes nothing about the app.

52. Call Management

Codename One can make a call your app carries look, to the operating system and to the user, like a call the phone itself placed. That means the lock-screen call UI, the ringtone that plays while your app isn’t running, the entry in the system call log, the audio session the OS hands over and takes back, and the caller’s name shown for an incoming number the address book has never seen.

The APIs live under com.codename1.call, in three sub-packages, and they’re separate packages rather than one because referencing a package is the whole opt-in. The build server decides what native machinery an app gets by scanning bytecode for these prefixes, so an app that only labels spam numbers pays for none of the calling machinery — no telephony permissions, no VoIP background mode. Referencing com.codename1.call itself costs nothing; it holds only the shared value types.

CapabilityiOSAndroidSimulator and desktopJavaScript

System call UI

yes (CallKit)

yes (Android 8 and later, see below)

simulated

 — 

Ring while the app isn’t running

yes (com.codename1.call.voip)

yes, from your own push handler

simulated

 — 

Answer, hold, mute, keypad from the system

yes

yes

simulated

 — 

Set the system mute state from your app

yes

 — 

simulated

 — 

Audio session handoff

yes

synthesized

simulated

 — 

Conference grouping

 — 

 — 

 — 

 — 

Caller identification

yes (Call Directory)

 — 

simulated

 — 

Call blocking

yes

yes (Android 10 and later)

simulated

 — 

Branch on the capability queries — Calls.isSupported(), Calls.getCapabilities(), VoipPush.isSupported(), CallDirectory.isSupported() — rather than on platform detection. There is no way to make a desktop show a lock-screen call, so an app that offers calling needs an in-app fallback screen whatever else it does.

Every callback in this family arrives on the EDT.

52.1. Five Things Worth Knowing Before You Design Around This

Codename One doesn’t carry the voice. There is no codec here, no signalling and no WebRTC: those are your app’s, and there are good libraries for them. What was missing was everything around the media, and without it an app couldn’t ring at all while backgrounded — which is why a Codename One app couldn’t previously be a calling app no matter how good its audio was.

Start media when the audio session activates, not when the user answers. This is the single most common way to get a calling app wrong. On iOS the system owns the audio session: your app configures a category but must not activate it, and CallKit activates it and tells you. Code that starts its audio engine in answerRequested works in every simulator and produces a silent call on a device, with no error anywhere. Use audioSessionActivated. The Android port synthesizes the same callback when the connection goes active, so the one piece of advice is correct everywhere.

Where to start call media in the callback sequence

A pushed call is already ringing. When a call arrives as a VoIP push, iOS requires it to be reported to the system before any of your code runs and kills the app when it isn’t — so native code does the reporting from a fixed payload, and your listener is told afterward. PushedCall therefore hands you a CallSession you never created, possibly one the user has already answered. Your job is to attach media to a call that exists, not to decide whether to ring.

The two platforms draw the ringing screen differently. On iOS CallKit presents the call and there is nothing to build. On Android a self-managed calling app gets no system call screen at all: Telecom arbitrates with other calls, routes the audio and writes the call log, but presenting the call is the application’s job. The port does that for you — a call-category notification carrying a full-screen intent, and the ringing screen that intent launches. On an unlocked device you get the notification as a heads-up banner; on a locked one the platform shows the ringing screen over the keyguard. Answer and Decline appear on both and are wired to the same listeners as everywhere else. The screen is a plain one, because it ships inside the port and has no resources of its own: if you want your own, present it from your incomingCall listener and it draws on top. Two consequences are yours: the labels come from the android.call.incomingTitle, android.call.answerLabel and android.call.declineLabel build hints and are English until you set them, and the user can turn the Incoming calls notification channel down in Settings. Anything below high importance files the notification in the shade instead of ringing, so the port treats it as it treats a revoked POST_NOTIFICATIONS: Calls.getAvailability() answers NOT_PERMITTED and Calls.reportIncoming is refused, rather than reporting a call that rings nowhere.

Implement providerReset. It means every call your app had is gone, because the user switched calling off or the platform recovered from an error. Drop your media and forget your sessions, but don’t call end() on them — they no longer exist as far as the system is concerned. An app that leaves this method empty leaks its media engine and shows calls that aren’t there.

52.2. Owning A Call

Configure once, then listen. Calls.configure must run before anything is reported: on Android an unregistered account makes TelecomManager ignore every reported call — no exception, no log line, no call.

if (!Calls.isSupported()) {
    return;                        // fall back to an in-app call screen
}
Calls.configure(new CallConfiguration().displayName("Acme Talk"));

Calls.addActionListener(new CallActionAdapter() {
    public void answerRequested(String callId, CallAction action) {
        signalling.accept(callId);
    }

    public void endRequested(String callId, CallAction action) {
        signalling.hangUp(callId);
        media.stop();
    }

    public void audioSessionActivated(CallAudioSession session) {
        media.start(session.getCallId());   // here, NOT in answerRequested
    }

    public void audioSessionDeactivated(String callId) {
        media.stop();
    }

    public void providerReset() {
        media.stopEverything();             // every call is gone
    }
});

An incoming call your app learned about over its own connection is reported with an identifier you allocate. Both ends must use the same one for the life of the call.

String callId = CallId.random();          // the id both ends will use
Calls.reportIncoming(callId, CallHandle.phone(theirNumber), "Ada Lovelace", false)
        .onResult((session, err) -> {
            if (err != null) {
                // The system refused to ring: an emergency call, another
                // app's call, or Do Not Disturb. Tell the caller.
                return;
            }
            signalling.invite(callId, theirNumber);
        });

Note the failure path isn’t decoration. The system genuinely refuses to ring a call during an emergency call, when another app holds one, or when the user has switched your app’s calling off, and Calls.getAvailability() will tell you before you try. An app that assumes reporting always succeeds shows a caller "ringing" over a phone that’s silent.

One difference between the platforms is worth designing around rather than discovering. On iOS the provider is configured for a single call, so while your app holds one, CallKit refuses the next report and getAvailability() answers THIS_APP_IN_CALL. Android Telecom accepts a second self-managed call from the same account, so it keeps answering AVAILABLE. If your app offers call waiting, branch on the availability rather than assuming a second call can always ring.

Outgoing calls are the same shape, with two extra reports that drive what the user sees: reportStartedConnecting while the far end is being rung, and reportConnected when media is actually flowing, which is what starts the call duration.

String callId = CallId.random();
Calls.reportOutgoing(callId, CallHandle.phone("+14155551212"), "Ada", false)
        .onResult((session, err) -> {
            if (err != null) {
                return;
            }
            session.reportStartedConnecting();     // we are ringing them
            signalling.place(callId, "+14155551212");
            // ... and once the far end picks up:
            session.reportConnected();             // starts the duration
        });

CallSession carries the rest: end, setHeld, setMuted, sendDigits, update and groupWith. Three are worth reading twice. setMuted tells the operating system what the mute button should look like and does not stop your app sending audio — nothing in this package touches media. It’s the one CallSession method the two platforms don’t agree on: check CallBridge.CAPABILITY_MUTE, which Android doesn’t offer, because a self-managed call there can’t tell Telecom its mute state and the call answers NOT_SUPPORTED. Hearing what the user does with the system’s mute button is a different thing and arrives on both through muteRequested. reportEndedRemotely rather than end is what you call when the hang-up came down your own signalling, so the system call log says the far end hung up instead of blaming the user.

And groupWith answers NOT_SUPPORTED everywhere, which is why the matrix above shows conference grouping as absent on both platforms. Neither lets an app conference two of its own calls: CallKit’s group action travels from the system to your app with no counterpart in the other direction, and Telecom conferences self-managed calls only through a ConnectionService conference Codename One doesn’t build. Nothing stops you mixing several streams into one call yourself and reporting that as a single call — that needs no permission from either platform, and it’s what apps that offer conferencing actually do.

52.3. The Values These APIs Hand You

Everything above takes or returns a handful of small types from com.codename1.call. Six are enumerations — CallState, CallDirection, CallEndReason, CallAvailability, CallHandleType and CallError — so a value that isn’t one of their constants won’t compile, though a switch over one isn’t required to cover every constant and an unhandled state falls through rather than failing the build. CallHandle and CallException are ordinary classes and CallId is a holder for static helpers whose identifiers are plain strings, so none of those three offer values() or anything else enum-shaped.

CallState

RINGING, DIALING, ACTIVE, HELD, ENDED. Incoming calls start at RINGING and outgoing ones at DIALING, but ACTIVE and HELD cycle in both directions — CallSession.setHeld(false) and a fulfilled resume both put a held call back to ACTIVE, so code that treats HELD as one-way rejects a legitimate resume or leaves media paused. ENDED is the only terminal state, and it’s where you release whatever the call was holding. The session’s asynchronous operations — end, setHeld, setMuted, sendDigits, groupWith — fail with CallError.INVALID_ID once a call has ended, which is how a bookkeeping bug in the app surfaces; the reporting methods that return void, such as reportConnected and update, return without a word instead, so don’t wait on them for that signal.

CallDirection

INCOMING or OUTGOING, fixed when the session is created and never changing after that. Read it when you hold a session you didn’t report yourself — a session passed to a listener, or fetched from Calls.getSessions() — rather than keeping your own note of the direction beside the call id.

CallEndReason

REMOTE_ENDED, LOCAL_ENDED, UNANSWERED, BUSY, FAILED, FILTERED. FILTERED covers more ground than its name suggests. It’s documented as the system declining the call rather than the user — Do Not Disturb, or a blocked number — and it differs from UNANSWERED in that the call never rang. The Android port also reports it for a fulfilled reject, though, so a Decline from the lock screen arrives as FILTERED there while a hang-up after answering arrives as LOCAL_ENDED. Read it as "this call ended without being taken, and not because the far end gave up," and don’t try to tell an explicit decline from a silent block out of this value alone.

CallAvailability

what Calls.getAvailability() answers before you report a call, and AVAILABLE is the only value that lets one ring. Check it before an incoming report; an outgoing call is a different question. On Android the notification gate that makes this answer NOT_PERMITTED applies to incoming calls alone — an outgoing call never rings, so reportOutgoing goes through and refusing it would take away a call the user started. The values need three different responses, not two. EMERGENCY_CALL_IN_PROGRESS and OTHER_APP_IN_CALL pass on their own, so a retry is the right move — THIS_APP_IN_CALL is in the same family, though Android’s bridge never reports it by design, because Telecom accepts a second call from the same account. NOT_CONFIGURED means Calls.configure hasn’t completed successfully, which is yours to fix in code and not something a user can clear; prompting for it strands them in front of a dialog that changes nothing. Read it as "not configured," not as "never called," because on Android a configure that threw answered UNAUTHORIZED and left the port without a registered account, so calling it again without fixing what it reported changes nothing. NOT_PERMITTED is Android only — iOS never answers it — and it covers three conditions with three different fixes, none of which is the calling switch. MANAGE_OWN_CALLS missing: that one is a normal permission granted at install, so there’s no prompt behind it and Calls.requestPermissions doesn’t ask. The builder declares it for you whenever the app uses the call packages, so its absence means something rewrote the manifest, and the fix is in the build rather than on the device. POST_NOTIFICATIONS not yet granted on Android 13 and later: that one is a runtime permission, asking for PERMISSION_NOTIFICATIONS is exactly right, and it’s the usual answer the first time an app rings. Notifications switched off for the app, or the incoming-call channel dropped below high importance: no permission is missing, so only the system’s own notification settings can clear it. Only UNSUPPORTED is permanent.

CallHandle and CallHandleType

who the call is with, and one of GENERIC, PHONE_NUMBER or EMAIL_ADDRESS. Build a handle with CallHandle.phone(…​), CallHandle.email(…​) or CallHandle.generic(…​) rather than by hand; the type travels with the value and decides how the system displays and dials it.

CallId

static helpers for the identifier both ends of a call have to agree on, which is a String rather than a type of its own. CallId.random() mints one, CallId.isValid(…​) checks one that arrived over your own connection, and CallId.normalize(…​) puts it in the form the platform expects.

CallError and CallException

why an action failed, as an enum and the exception that carries it. Don’t sort these into "this call" and "this app" — the codes cut across that line, and what to do about one depends on the condition behind it rather than on the code alone.

CALL_REFUSED is the system declining to place or ring the call right now, and it spans both kinds of cause: an emergency call or another app’s self-managed call passes on its own, while the user having switched your app’s calling off, or Calls.configure not having run, refuses every report until something changes. Nothing in the code tells you which you got: Android’s refusal carries one message naming all three causes. Calls.getAvailability() is worth asking first, and won’t always separate them either — it never checks whether the registered PhoneAccount was disabled, and its foreign-call test needs READ_PHONE_STATE, which the port doesn’t declare by design and treats as "no" when absent. It can answer AVAILABLE and the report still come back CALL_REFUSED. Retry on a timer that slows down rather than reading anything into a repeat: the emergency or foreign call that caused the first refusal is often still up for the second, and a persistent refusal looks identical.

CALL_FILTERED means the system suppressed the call before it rang — Do Not Disturb on iOS, or a blocked number — so no action will arrive for it. DUPLICATE_CALL means that identifier is already live. Calls.report checks its own session map first, so a second report of an id your app already has fails here and never reaches a port — retrying it fails identically every time. Fetch the existing session and update that instead. The reconciliation the ports do, folding a repeat into an update, is for reports arriving from the native side, not for this call.

NOT_SUPPORTED is either the whole integration or the one operation you asked for. Calls.isSupported() covers the first — Android self-managed calls need API 26, CallKit is absent from tvOS and watchOS — so branch on it rather than waiting for the error. It doesn’t cover the second: setMuted answers NOT_SUPPORTED on Android because Telecom won’t let a self-managed call set its own mute state, and groupWith answers it on every port today. Disable the capability that came back unsupported, not calling as a whole.

UNAUTHORIZED comes from two places on Android and nowhere else: a SecurityException out of configure or a report, which means MANAGE_OWN_CALLS is missing, and an incoming report refused because notifications are off or the incoming-call channel sits below high importance. That second one is two conditions wearing one code, and they part exactly as they do under NOT_PERMITTED above: a POST_NOTIFICATIONS the user hasn’t granted is cleared by asking, while notifications switched off or a lowered channel is a setting no prompt reaches. Ask once, and if the code comes back with the permission already granted, stop asking — the recovery is in settings. A denied microphone grant or a declined call-screening role doesn’t arrive here — Calls.requestPermissions resolves with the bitmask it got and CallDirectory.requestScreeningRole resolves false, so read those results rather than waiting for an error.

BUSY is about neither a call nor the app: a conflicting operation is already running, which can be another call but can equally be an unrelated request holding a shared channel — CallDirectory.requestScreeningRole() answers it when another dialog owns the activity result — so retry the operation rather than blaming a session. And PROVIDER_RESET says the call provider was reset while that one operation was in flight, so it could never be answered; the provider works again afterward and the next report may well succeed.

52.4. Actions, And Why They Have To Be Answered

How a CallAction is answered, and what an unanswered one costs

When the user answers on the lock screen, hangs up from the car, or taps the keypad, the request arrives with a CallAction. Both platforms require an answer within a few seconds, and an unanswered one times out and leaves the system UI and your app disagreeing about the call, with nothing in the log to say so.

Almost every app should ignore this entirely: an action the listener doesn’t touch is fulfilled automatically, which is the correct behaviour. Only an app that must do slow asynchronous work before it knows whether it can comply — renegotiating a session, say — calls action.defer() and then fulfill() or fail() itself. A deferred action that’s forgotten is failed by a safety timer, because a failed action puts the UI back in a state the user can act on and a timed-out one doesn’t.

52.5. Ringing When The App isn’t Running

A VoIP push is delivered straight to the calling machinery, launching the app if it has to. It’s the only way an app that isn’t running can make the phone ring.

VoipPush.setListener(new VoipPushListener() {
    public void callReceived(PushedCall call) {
        if (call.isStale()) {
            history.addMissed(call.getHandle());   // it is already over
            return;
        }
        // The phone is ALREADY ringing. Attach media and wait for the
        // ordinary answerRequested; do not report the call again.
        signalling.attach(call.getSession().getCallId(), call.getData());
    }

    public void tokenChanged(String token) {
        signalling.registerVoip(token);
    }
});
VoipPush.register();

com.codename1.call.voip is iOS-only, and that isn’t a gap. It exists because iOS reports a pushed call to the system before any of your code runs and kills the app when it doesn’t — a constraint Android doesn’t have. On Android a high-priority FCM message reaches your ordinary push callback like any other, and you call Calls.reportIncoming from there. VoipPush.isSupported() answers false on Android so you can branch on it.

Because the call is reported to the system before any Java runs, the native code parses the push itself and your server has to honour a fixed shape:

{ "cn1call": { "uuid": "6B29FC40-CA47-1067-B31D-00DD010662DA",
               "handle": "+14155551212", "handleType": "phoneNumber",
               "displayName": "Jane Doe", "video": false,
               "ttl": 30, "data": "opaque, handed back untouched" } }

uuid and handle are required; everything else falls back to the build hints. data is never parsed by the framework and is how you carry a room id or a session token through to code that runs later. To retract a call that was cancelled before it was answered, send the same uuid with "cancel": true.

Sending a payload without cn1call wakes the app and rings nothing, which on iOS is the case that gets the app killed.

Set the listener before or after registering, as you like — calls that arrived first are held and drained when a listener appears. An app that never sets one will find its calls timed out and ended by the platform.

UIBackgroundModes: voip is added only for apps that reference com.codename1.call.voip, and Apple rejects an app that carries it without a working call implementation. That’s the entire reason this is a separate package from com.codename1.call.session.

52.6. Naming And Blocking Other People’s Callers

This is caller ID and spam blocking: an ordinary cellular call arrives, and the system asks the installed directories whether any of them recognizes the number. It has nothing to do with calls your app carries, which is why it’s a package of its own and carries no telephony permissions.

DirectoryEntry[] entries = {
    new DirectoryEntry(14155551212L, "Acme Support"),
    new DirectoryEntry(14155559999L, null, true)      // blocked outright
};
CallDirectory.setEntries(entries).onResult((ok, err) -> {
    if (err == null) {
        CallDirectory.reload();
    }
});

Numbers are long rather than String on purpose. Both platforms require the list in ascending numerical order and reject the whole list — not the offending row — when it isn’t, and a string list sorts lexicographically, which puts +1999…​ before +12…​. CallDirectory sorts and de-duplicates what you give it, so you need not.

Three platform facts to design around. On iOS the numbers are read by a separate app extension the system starts on its own schedule, so installing the data and asking the system to read it are two steps. Caller identification is off until the user enables your app in Settings, which nothing you write can do for them — check DirectoryStatus.isEnabled() before concluding that a load failed.

And Android blocks but doesn’t label. A CallScreeningService may allow, reject or silence a call; Android gives a third-party app no way to put a name on one. blocked entries work there and labels are ignored, which is why Calls.getCapabilities() reports CAPABILITY_SCREENING and not CAPABILITY_DIRECTORY on Android.

52.7. Developing Without Hardware

The simulator and the desktop builds run a real simulated call stack rather than reporting the feature unsupported, so a ringing screen, an in-call screen and a media lifecycle can all be built and tested without a phone.

It’s a simulation and not a stub, and the difference is deliberate: nothing completes inside the calling stack frame, and the audio session arrives well after the answer rather than with it, because that gap is where media bugs live.

The Simulate → Calls menu scripts the awkward cases rather than the happy one, which is what makes it worth opening:

  • Ring A Second Call While Busy — where a single-call state machine breaks.

  • Refuse The Next Call — the emergency-call refusal an app that assumes reporting always succeeds never handles.

  • Never Activate The Audio Session — makes the silent-call mistake above reproduce on your desk instead of on a device.

  • Queue A Call That Already Ended — the cold-start call that must be logged as missed, never answered.

Every item is also callable from a test with CN.execute("call:itemN").

52.8. Build Hints

HintDefaultWhat it does

ios.call.providerName

the app’s display name

The name shown above a call in the system UI.

ios.call.ringtone

the system ringtone

A sound file in the bundle to ring with.

ios.call.icon

none

A template image shown beside the call.

ios.call.unknownCaller

none

The name shown when a push carries no displayName.

ios.call.pushTTL

30

Seconds a pushed call waits for your code before the system ends it as unanswered.

ios.call.video

false

Whether the provider offers video calls.

ios.call.recents

true

Whether calls appear in the system call log.

android.call.incomingTitle

Incoming call

The text on the Android ringing notification.

android.call.answerLabel

Answer

The Android answer button.

android.call.declineLabel

Decline

The Android decline button.

android.call.channelName

Incoming calls

The notification channel the user sees in Settings.

call.video

false

Whether video calls are offered, on both platforms. Overridden per platform by ios.call.video and android.call.video.

android.call.video

false

Whether the Android manifest declares CAMERA. Required for video calls.

videoSupported(true) on CallConfiguration isn’t enough on its own for a video call. The camera permission has to be in the manifest, and a manifest is written at build time, so the build has to be told: set call.video (or android.call.video) as well. Without it Android refuses to grant CAMERA — a permission the manifest never asked for can’t be granted, and the request resolves denied every time. Calls.getCapabilities() won’t report CAPABILITY_VIDEO on such a build, so branch on that rather than assuming the configuration was enough.

The ios.call. hints are baked into Info.plist rather than set from code because native code needs them during launch, before any of your code has run. The android.call. labels are read when the call rings, and exist because a ringing screen is the last place to show an untranslated English string. Setting only CallConfiguration and expecting a pushed call to ring with the right name is the mistake to avoid; CallConfiguration refines them once the app is up.

Everything else is automatic. Referencing com.codename1.call.session links CallKit, registers the Android service, and declares the two Android permissions the ringing notification needs — POST_NOTIFICATIONS and USE_FULL_SCREEN_INTENT, the second of which Google restricts to calling and alarm apps and which is injected here on the strength of the package you referenced; referencing com.codename1.call.voip adds PushKit and the voip background mode; referencing com.codename1.call.directory generates the iOS Call Directory extension and the Android screening service.

CallKit can’t be used in apps distributed in mainland China, and an app that ships it to that storefront is rejected or removed. Codename One can’t detect where you intend to publish, so if that applies to you, keep the calling packages out of that build.

53. VPN

Two quite separate things are called VPN support, and conflating them wastes a great deal of time.

The first is asking the operating system to run a standard tunnel — an IKEv2 configuration the platform implements itself, which your app installs, starts, stops, and watches. That’s portable, needs a capability any paid developer account can switch on, and is what almost every app that says "connect to VPN" actually wants. It’s com.codename1.vpn.profile, and it’s what this chapter is mostly about.

The second is shipping a tunnel of your own that receives raw IP packets and decides what to do with them. That’s com.codename1.vpn.tunnel. It runs on Android with no configuration at all, and on iOS behind a build hint and an entitlement Apple grants case by case; the section below covers both, and what to write so the loop you wrote is the loop that runs on either.

CapabilityiOSAndroidSimulator and desktopJavaScript

Install a managed IKEv2 profile

yes

yes (Android 11 and later)

simulated

 — 

IPsec with a pre-shared key

yes

 — 

simulated

 — 

Connect, disconnect, observe status

yes

yes (Android 11 and later)

simulated

 — 

Always-on / on-demand

yes

yes

simulated

 — 

A tunnel your app implements

yes (see below)

yes

simulated

 — 

Detect that some VPN is active

yes

yes

yes

 — 

Branch on Vpn.isSupported() and Vpn.getCapabilities() for the managed profile, and on Tunnels.isSupported() for a tunnel of your own, rather than on the platform. Every callback arrives on the EDT.

Which of the three VPN questions your app is asking

53.1. Detecting A VPN Is A Different Question, And Already Answered

If all you want to know is whether this device’s traffic looks like it’s going over a VPN, NetworkManager.isVPNActive() has answered that for years, works on far more platforms than can install a profile, and needs no entitlement at all. Check isVPNDetectionSupported() first, because not every platform implements it.

Its own contract calls the answer advisory: the platform APIs and interface-name heuristics behind it miss some VPN configurations and report some non-VPN tunnels as VPNs. Warn on it, log it, degrade on it — but don’t build enforcement on it, because both kinds of mistake are available to it and neither is rare enough to design around.

if (NetworkManager.getInstance().isVPNActive()) {
    // traffic is being tunnelled by something
}

Reach for this chapter only when your app needs to configure a VPN, not when it needs to know about one.

53.2. Installing And Running A Managed Profile

if (!Vpn.isSupported()) {
    return;                          // no managed VPN on this platform
}
VpnProfile profile = new VpnProfile("vpn.example.com")
        .protocol(VpnProtocol.IKEV2)
        .remoteIdentifier("vpn.example.com")
        .localIdentifier("alice")
        .usernamePassword("alice", secret)
        .displayName("Acme Corporate");

Vpn.install(profile).onResult((ok, err) -> {
    if (err instanceof VpnException
            && ((VpnException) err).getError() == VpnError.USER_DECLINED) {
        return;                      // ordinary outcome, not a failure
    }
    if (err == null) {
        Vpn.start();
    }
});

Three things about that code are worth spelling out.

The user may be asked, and you can’t skip the asking. The platform shows a system prompt when it wants consent, and neither lets an app avoid it. A declined prompt fails with VpnError.USER_DECLINED, which is an ordinary outcome and not something to report as an error — the snippet treats it as a normal return.

It isn’t a prompt on every install, though. Android asks once and remembers, so replacing a profile in an app the user has already approved goes through with nothing shown. Be ready for the prompt; don’t build a flow that depends on it appearing, and don’t tell the user a dialog is coming.

One configuration per app. Both platforms give an app a single managed configuration, so install replaces whatever was there rather than adding to it.

The platform keeps the secret. A profile read back with Vpn.load() describes the configuration but its getPassword() is always null, because the password lives in the platform keychain and is never handed back. Use isPasswordKnown() to tell a profile built without a password from one that was loaded, where the platform kept it".

Watching the tunnel is a listener, and worth having: it can go down without your app asking.

Vpn.addStatusListener(status -> {
    switch (status) {
        case CONNECTING:
            ui.showConnecting();
            break;
        case CONNECTED:
            ui.showConnected();
            break;
        default:
            ui.showOffline();        // it can drop without being asked
            break;
    }
});

Note the states aren’t decoration either. CONNECTING can last as long as a real negotiation takes, and an app that only watches for CONNECTED looks frozen for the duration.

53.3. Where The Platforms Disagree

Android needs version 11. VpnManager and Ikev2VpnProfile arrive in API 30. Codename One doesn’t raise your app’s minSdkVersion for this — a feature that can report itself absent isn’t worth cutting off older devices for — so Vpn.isSupported() answers false below that and your app needs a path for it.

Android offers IKEv2 only. There is no pre-shared-key IPsec equivalent in the managed profile API. A profile asking for VpnProtocol.IPSEC is refused with INVALID_CONFIGURATION rather than installed as something else without comment.

Starting isn’t the same as connected. On Android the platform accepts the start request and reports nothing further, so a successful start() means the request was accepted, not that the tunnel is up. Watch the status.

53.4. A Tunnel Your App Implements

If your VPN speaks a protocol the platforms don’t implement — anything that isn’t IKEv2 or IPsec — you write the packet loop yourself, in Java, once. Extend VpnTunnel and hand it to Tunnels.start.

Tunnels.isSupported() answers true on Android, true on an iOS build that generated the extension, and false everywhere else — including an iOS build that didn’t ask for one, which is every iOS build by default. Ask it, and keep a path for the answer being no: Tunnels.start refuses with NOT_SUPPORTED rather than pretending.

The addresses in a TunnelSetup are read before the platform is asked. An address, a route or a DNS server that isn’t an IP literal fails the start with INVALID_CONFIGURATION, and the message names the field. A setup with no address at all fails the same way, because a link with no address isn’t one a platform can establish. That check runs in Tunnels.start on every platform, so a setup the simulator accepts is a setup a device accepts.

On Android the tunnel runs inside a VpnService in your app’s own process. The instance you passed to Tunnels.start is the instance that runs, and everything it closed over is still there.

On iOS it runs inside a Network Extension: a separate process with its own bundle identifier, its own provisioning profile and a hard memory limit. The build generates that extension for you — a target of its own, carrying a virtual machine, running the class you name in ios.vpn.tunnel.class.

The extension is its own translation, rooted at your tunnel rather than at your app. That’s what makes it possible at all: an app extension compiles with APPLICATION_EXTENSION_API_ONLY, and the iOS port’s own natives call UIApplicationMain and [UIApplication sharedApplication], which an extension may not touch — so a target carrying the application’s translation couldn’t be built. Rooted at the tunnel it carries what the tunnel reaches, and the application shell is simply not in it.

The extension has no networking stack. It carries the translated program and the virtual machine, and nothing else — so com.codename1.io.Socket, ConnectionRequest and everything else that reaches Util.getImplementation() finds nothing there. ParparVM’s java.net is URI and URL; there are no sockets in it either.

What an iOS tunnel can do, then, is everything that happens on the device: see every packet, inspect it, rewrite it, drop it, answer it with forward. That covers filtering, local DNS, per-destination decisions and anything else your own code can decide from the packet and from TunnelSetup.data.

What it can’t do is relay to a remote VPN server, because it has no way to open the connection. On Android it can — the tunnel runs in your app’s own process, where the implementation is installed — so the same tunnel class is not equally useful on both. Write the relay behind an interface if you need one platform to do it and the other to fall back.

A Mac Catalyst slice reports the tunnel unsupported, and that’s deliberate: the extension target is built for the iOS destination only, so the Mac app has no provider to start. Tunnels.isSupported() answers false there, and the managed profile in com.codename1.vpn.profile still works.

Which turns the advice below into a build error rather than a disappointment. Write the tunnel as though it ran in a process of its own, because it does: no statics you set, no Display, no open connections, and the tunnel constructed fresh with none of them — iOS constructs it, so it needs an accessible no-argument constructor. A tunnel that reaches for your app’s other classes drags them into this translation, and the ones backed by native code fail the extension’s link, naming the symbol they wanted. That includes a NativeInterface from a cn1lib: the extension carries the translated program and the virtual machine, and nothing the port or a library hand-wrote.

Two things are yours to arrange, and neither is something a build can do for you. Apple grants com.apple.developer.networking.networkextension case by case rather than from the developer portal, so setting ios.vpn.tunnel=true is you saying you hold it — an App ID without it fails codesigning with an error naming the entitlement and not the reason it appeared.

Both App IDs need it, which surprises people. The extension carries the entitlement because it is the provider; the app carries it because NETunnelProviderManager — what Tunnels.start uses to save the configuration and start the tunnel — is Network Extension API too. Signing only the .appex leaves the app unable to start its own tunnel: every attempt fails with a permission error. The build writes the entitlement into both, and refuses before the archive if either profile doesn’t grant it.

And the extension signs under its own App ID, <packageName>.vpntunnel, so a device archive needs a provisioning profile for that identifier passed as ios.appext.CN1VpnTunnel.provisioningData or .provisioningURL, exactly as the Call Directory extension does. Override the identifier with ios.vpn.tunnel.buildSettings.PRODUCT_BUNDLE_IDENTIFIER if you must; it has to stay inside the app’s own namespace, because Apple rejects an embedded extension that isn’t.

TunnelSetup.data exists for that reason, and the rule worth taking away follows from it:

  • Everything the tunnel needs arrives through VpnTunnel.onStart.

  • Anything it reaches for outside that becomes a static, which works while the tunnel shares the app’s process and goes missing the moment it doesn’t.

When the iOS half lands it will also need the com.apple.developer.networking.networkextension entitlement, which Apple grants case by case rather than self-serve.

53.5. Developing Without Hardware

The simulator and the desktop builds run a simulated configuration store that tunnels nothing, so an install flow, a status screen and a reconnect button can all be built without a device or a server.

A packet tunnel runs there too, over a loopback: Tunnels.start calls your onStart, packets you feed in reach onPacket, and what you forward comes back out where a test can read it. The transport is fake and the packet loop is the real one, which is what makes a tunnel testable at all — neither platform lets you run one on a desktop.

Like the call simulation it keeps the awkward orderings: connecting passes through CONNECTING, and a profile read back has no password, because a simulation that handed the secret back would let code depend on something no device does.

The Simulate → VPN menu scripts the failures:

  • Decline The Next Install Prompt — the outcome apps routinely treat as impossible.

  • Refuse The Credentials On Connect — the install and the connection fail in different places for different reasons, and apps commonly handle only the first.

  • Drop The Tunnel — a tunnel going down without being asked.

Every item is also callable from a test with CN.execute("vpn:itemN").

53.6. Build Hints

HintDefaultWhat it does

ios.entitlements.com.apple.developer.networking.vpn.api

injected as allow-vpn

The Personal VPN capability. Enable it on the App ID or the app won’t sign.

ios.vpn.tunnel

false

Generates the packet tunnel extension. Also your assertion that the App ID holds the Network Extension grant; see above.

ios.vpn.tunnel.class

none

Which VpnTunnel subclass the extension runs, fully qualified. Required when the hint above is true, and checked against the compiled classes.

ios.appext.CN1VpnTunnel.provisioningData

none

The extension’s own provisioning profile, base64, for a device archive. .provisioningURL is the hosted alternative.

ios.vpn.tunnel.buildSettings.<KEY>

none

Overrides a build setting on the generated target, as the other extensions' equivalents do.

Referencing com.codename1.vpn.profile links NetworkExtension.framework and injects the Personal VPN entitlement with no hint at all.

Codename One never injects com.apple.developer.networking.networkextension from a class reference. Apple grants it case by case rather than self-serve, and an entitlement an App ID doesn’t carry fails codesigning with an error naming the entitlement and not the reason it appeared. That’s why the hint exists at all rather than the package alone turning the extension on: the hint is the assertion that both App IDs hold the grant, and it’s on that assertion — and only then — that the build writes the entitlement into the app and into the extension.

Android needs no hints. Referencing the package adds the VpnService, its BIND_VPN_SERVICE declaration and the foreground-service permissions the platform requires to keep a tunnel running.

54. Health

Codename One ships a cross-platform health API under com.codename1.health that reads and writes the platform health store — HealthKit on iOS and watchOS, Health Connect on Android — summarizes data over time, watches it for changes, records workouts, and streams live measurements from standard Bluetooth health sensors.

Health.getInstance() is the single entry point and never returns null: ports without health support return a fallback whose operations fail fast with HealthError.NOT_SUPPORTED, so calling code needs no platform-specific if.

Every callback — AsyncResource results, sensor samples, workout events, change batches — is delivered on the EDT, on every platform. That includes the local-backed ports: the simulator, desktop and JavaScript stores hand results back on the EDT rather than on whichever thread called, so a read started from a worker thread can update the UI from its callback directly, with no callSerially of your own. A background change delivery may arrive with no visible UI, after the OS relaunched your app, but it’s still on the EDT.

CapabilityiOS 26+iOS < 26watchOSAndroidSimulatorDesktop / JavaScript

Read samples and aggregates

yes

yes

yes

yes

yes (scripted)

local only

Write samples

yes

yes

yes

yes

yes

local only

Read-authorization introspection

no

no

no

yes

yes

n/a

Background push delivery

no

no

no

no

no

no

Change delivery while running

yes

yes

yes

yes

no

no

Live workout session

recorded

recorded

recorded

recorded

recorded

recorded

Automatic sensor collection in a workout

no

no

no

no

no

no

Bluetooth health sensors

yes

yes

limited

yes

yes

yes

Subscriptions are the one place the local-backed ports aren’t a substitute for a device. subscribe() registers, persists its cursor and restores across launches everywhere, but nothing on the local store records a mutation as a change, so drainChanges() there resolves with zero and no listener ever fires. Subscription code can be written and its registration exercised on the desktop; whether it actually delivers has to be tested on a phone.

Branch through the capability queries — Health.getAvailability(), HealthStore.isTypeSupported(…​), WorkoutManager.isLiveSessionSupported(), HealthSubscription.isPushDelivery() — rather than through platform detection.

54.1. Quick start

Health health = Health.getInstance();
HealthAvailability availability = health.getAvailability();
if (availability == HealthAvailability.PROVIDER_NOT_INSTALLED
        || availability == HealthAvailability.PROVIDER_UPDATE_REQUIRED) {
    health.openProviderSetup();
    return;
}
if (availability == HealthAvailability.NOT_SUPPORTED) {
    return;
}
HealthStore store = health.getStore();
store.requestAuthorization(
        HealthAccess.read(HealthDataType.STEPS),
        HealthAccess.read(HealthDataType.HEART_RATE))
     .onResult((asked, err) -> {
         if (err != null) {
             Log.e(err);
             return;
         }
         // asked == true means the user has now been ASKED, not that
         // anything was granted. On iOS the sheet completes identically
         // whether they enabled every switch or none.
     });

Check getAvailability() before anything else. On Android it distinguishes a working provider from one that’s missing or out of date, and both of those are recoverable by the user through openProviderSetup(). On the desktop and JavaScript ports it returns LOCAL_ONLY, meaning reads and writes work and are durable but the data is only ever your own app’s.

54.2. Permissions, and the one thing that will surprise you

Why an empty health result never proves refusal

You can’t ask whether you are allowed to read.

HealthKit doesn’t disclose read authorization. A read the user denied returns an empty result, indistinguishable from having no data at all, because an app that could tell the two apart could infer that a user is hiding a pregnancy or a prescription. That’s a privacy guarantee, not a gap in this API.

Three consequences follow, and all three are load-bearing:

  1. requestAuthorization(…​) resolving true means the user has been asked, not that anything was granted. On iOS the sheet completes identically whether they enabled every switch or none of them.

  2. getReadAuthorizationStatus(…​) returns UNKNOWN on iOS in every case. Android answers, because its read permissions are ordinary runtime grants; the Android port doesn’t pretend otherwise, and neither should your UI.

  3. There is no hasReadPermission() anywhere in this API, because on iOS it would be a lie.

HealthStore store = Health.getInstance().getStore();

// Write authorization is answerable on both platforms.
HealthAuthorizationStatus write =
        store.getWriteAuthorizationStatus(HealthDataType.BODY_MASS);

// Read authorization is NOT. On iOS this is always UNKNOWN, by design.
HealthAuthorizationStatus read =
        store.getReadAuthorizationStatus(HealthDataType.STEPS);
if (read == HealthAuthorizationStatus.UNKNOWN) {
    // The only honest probe: ask whether any data actually came back.
    store.hasAnyData(HealthDataType.STEPS, HealthTimeRange.lastDays(7))
         .onResult((hasData, err) -> {
             if (err == null && !hasData.booleanValue()) {
                 // Denied OR genuinely empty -- indistinguishable.
                 // Say "no data available", never "you denied access".
                 Health.getInstance().openHealthSettings();
             }
         });
}

hasAnyData(…​) is the only honest probe, and its name says exactly what it measures. Note also that on iOS your own writes stay readable to you even when read access was denied, so a true here doesn’t prove you can see other apps' data.

Never tell a user "you denied access" on the strength of an empty result. Say "no data available," and offer openHealthSettings(). Getting this wrong produces an app that accuses users of something they didn’t do.

Request the narrowest set of types you actually need, at the moment you need them. Both stores present every requested type on one sheet, and a long list at first launch is the most common reason people decline.

54.3. Reading samples

SampleQuery query = new SampleQuery()
        .addType(HealthDataType.HEART_RATE)
        .setTimeRange(HealthTimeRange.lastHours(24))
        .setSortDescending(true)
        .setLimit(500);

Health.getInstance().getStore().readSamples(query)
        .onResult((samples, err) -> {
            if (err != null) {
                Log.e(err);
                return;
            }
            for (HealthSample s : samples) {
                QuantitySample hr = (QuantitySample) s;
                // Reading a value forces you to name the unit, which is
                // what removes the whole wrong-unit class of bug.
                double bpm = hr.getValue(HealthUnit.COUNT_PER_MINUTE);
                Log.p(hr.getStartMillis() + ": " + bpm + " bpm");
            }
        });

Values always come back in the type’s canonical unit unless the query asks for another, so the two ports return identical objects. Reading a number out of a HealthQuantity requires naming the unit at the call site — there is no zero-argument getValue() — which removes the entire class of bug where pounds are read as kilograms and every downstream chart is wrong by a factor of 2.2.

On Android a limit applies per data type when a query names several: Health Connect pages per record type, so a limit of ten over two types can return twenty. Query one type per call when the cap has to be exact, which is what iOS does anyway — HKSampleQuery reads one type per query.

Always set a limit for high-frequency types. A year of continuous heart rate is on the order of half a million samples; the default cap of 10,000 exists so a naive query can’t exhaust the heap, and readSamplePage(…​) is there for walking more than that.

Health Connect stores a heart-rate series as one record containing many samples, while HealthKit returns many independent samples. SampleQuery.setFlattenSeries(boolean) defaults to true so both platforms hand back plain QuantitySample objects. Turn it off to keep a Health Connect record whole, which is what you want when you intend to delete it: you get one SeriesSample carrying the record identifier and every measurement in it. HealthKit has no series records at all, so iOS answers with plain samples whichever way the flag is set. Every flattened measurement still carries its record identifier, so record-level deletion works either way — and isDeletable(…​) is a separate question from isWritable(…​), so the series-shaped types Health Connect won’t let you write can still be deleted.

Writing a SeriesSample to a phone stores its measurements individually — neither platform accepts a series through this API — so the write result carries one identifier per measurement rather than one for the record. A series is unit-checked and converted exactly like a scalar write, and one longer than the platform’s batch limit is split across several calls. Sleep and workout samples can’t be written to HealthKit or Health Connect at all; writing one is refused with TYPE_NOT_SUPPORTED rather than reported as a success that stored nothing.

Sleep isn’t readable on either phone in this release. The iOS type map carries quantity types only, so HealthDataType.SLEEP is refused before a query runs, and SampleQuery.setSleepSessionGapMillis(long) — which configures the session reassembly that port will eventually need — changes nothing today. Android refuses it too: sleep is absent from the readable type set and the bridge has no record class for it, so a query fails with TYPE_NOT_SUPPORTED rather than returning nothing. Sleep works fully against the local and simulator stores, which is where to develop against it for now.

Heart-rate variability is two different statistics with one name. HealthKit stores SDNN, and stored only SDNN until iOS 27 added RMSSD; Health Connect has only ever stored RMSSD. HealthDataType.HEART_RATE_VARIABILITY_RMSSD is therefore the cross-platform one and the one to reach for, and it’s the first HRV type Android can answer at all — HEART_RATE_VARIABILITY_SDNN has no Health Connect record behind it and is refused there with TYPE_NOT_SUPPORTED. RMSSD on iOS needs both an iOS 27 device and an application built with the iOS 27 SDK, because the identifier doesn’t exist in older SDKs; anything else reports TYPE_NOT_SUPPORTED rather than substituting SDNN.

54.4. Aggregates and time buckets

AggregateQuery query = new AggregateQuery()
        .addType(HealthDataType.STEPS)
        .addMetric(AggregateMetric.TOTAL)
        .setTimeRange(HealthTimeRange.calendarDays(7, ZoneId.systemDefault()))
        // Calendar days, not a fixed 24 hours: a day is 23 or 25 hours
        // across a daylight-saving transition.
        .setBucket(HealthInterval.calendarDays(1, ZoneId.systemDefault()));

Health.getInstance().getStore().aggregate(query)
        .onResult((buckets, err) -> {
            if (err != null) {
                Log.e(err);
                return;
            }
            for (AggregateResult bucket : buckets) {
                HealthQuantity total = bucket.get(HealthDataType.STEPS,
                        AggregateMetric.TOTAL);
                if (total == null) {
                    // No data for that day. NOT the same as zero steps --
                    // render a gap rather than a bar at zero.
                    continue;
                }
                Log.p(bucket.getBucketStartMillis() + ": "
                        + (long) total.getValue(HealthUnit.COUNT));
            }
        });

Two rules here are easy to get wrong and hard to notice.

A bucket with no data returns null, never zero. A day on which no data was recorded and a day on which the user genuinely took no steps are different facts. Substituting zero turns an absence of data into a claim that nothing happened and draws a flat line through every day the phone stayed in a drawer.

Use calendar buckets when your UI labels them with dates. HealthInterval.calendarDays(1, tz) follows the calendar; a fixed 86_400_000 doesn’t, and drifts against the dates the user sees twice a year at each daylight-saving transition. Calendar intervals require an explicit TimeZone — nothing in this API reads the JVM default, because a server-side default of UTC would file a user’s evening walk under the wrong day.

Overlapping sources are counted twice, on every platform including iOS. When a phone and a watch both record steps for one walk, a total over them counts the walk twice. HealthKit’s statistics engine does de-duplicate overlapping sources, but no port uses it in this release — every metric is computed by shared code from raw samples, so the bucket arithmetic has one implementation rather than one per platform that can drift, and iOS double-counts exactly as Android does. This API leaves that visible rather than papering over it with a heuristic, because guessing which of two overlapping sources is authoritative is exactly the kind of quiet wrongness health data can’t afford. Use addSource(…​) to pin a query to the source you trust, and tell the user which device a figure came from.

54.5. Writing samples

QuantitySample weight = QuantitySample.create(
        HealthDataType.BODY_MASS,
        new HealthQuantity(178.4, HealthUnit.POUND),
        System.currentTimeMillis());
weight.setRecordingMethod(RecordingMethod.MANUAL_ENTRY);

Health.getInstance().getStore().write(weight)
        .onResult((result, err) -> {
            if (err instanceof HealthException
                    && ((HealthException) err).getError()
                            == HealthError.UNAUTHORIZED) {
                Health.getInstance().openHealthSettings();
                return;
            }
            if (result != null && result.hasRejections()) {
                // A batch can partially succeed; surface what was dropped
                // rather than discarding it silently.
                for (String reason : result.getRejections()) {
                    Log.p("rejected: " + reason);
                }
            }
        });

Writes are validated before the platform is touched: a type this app can’t write, an instantaneous sample of a cumulative type such as STEPS, or a quantity whose unit measures the wrong dimension all fail immediately with a message naming the offending sample, rather than as an opaque platform error later. Large batches are chunked automatically to the platform’s limit.

Set RecordingMethod on what you write. Other apps use it to decide how much to trust a value, and a manually typed weight is a different kind of evidence from one a scale reported.

HealthSample.getId() is assigned by the platform, is scoped to this install, and won’t survive a reinstall, so don’t use it as a primary key on your server. Keep your own identifier in your own storage and correlate on that. getMetadata() looks like the natural home for it and isn’t: metadata round-trips through the local and simulator stores but isn’t written to HealthKit or Health Connect in this release, so on a phone the identifier is gone the moment you read the sample back.

54.6. Background delivery

SubscriptionRequest request = new SubscriptionRequest("steps-v1")
        .addType(HealthDataType.STEPS)
        .setIncludeDeletions(true);

HealthSubscription sub = Health.getInstance().getStore()
        .subscribe(request, StepWatcher.class);

if (!sub.isPushDelivery()) {
    // Android: Health Connect never wakes the app. Drain from your
    // background-fetch handler instead of assuming push.
    Display.getInstance().setPreferredBackgroundFetchInterval(900);
}

The subscription id keys the persisted cursor that records how far you have read. Reuse it across launches and app updates and you resume exactly where you left off; change it and the framework treats it as new and resynchronizes from scratch. Hard-code it, and version it explicitly ("steps-v1") if you ever want a clean start.

No platform wakes your app for health data today. isPushDelivery() answers false everywhere, and that’s the honest answer rather than a placeholder: nothing arrives while your app is closed, so your app has to ask.

On Android this is permanent. Health Connect has no push mechanism at all and Google’s own guidance is to poll. On iOS, HealthKit does offer HKObserverQuery with background relaunch, but this release doesn’t register one, so iOS behaves the same way as Android for now. isPushDelivery() is the query to branch on; it will start answering true on iOS when observers land, and code that already polls will simply poll less often.

Draining is what delivers changes, and your app has to ask for it. Nothing here hooks the application lifecycle: call drainChanges() when you come to the foreground, and from your background-fetch handler. Omit it and a subscription delivers nothing at all, however much data arrives. This is the one place where forgetting a call looks exactly like having no new data.

The two backends drain differently, and the difference is visible to you. Android uses Health Connect change tokens, so a drain reports deletions as well as additions and never re-reports a sample you have already seen. iOS re-reads each subscribed type over the window since the last drain: additions arrive, deletions don’t, and a sample edited in place looks like a new one. Both advance their cursor only after your listener returns, so a crash re-reads rather than skips.

On iOS a subscription can miss backdated data, and there is no way around it in this release. The cursor is a timestamp, so it finds samples by when they were measured, not by when they arrived in the store. A watch that syncs an hour late, or a reading the user types in for yesterday, lands entirely behind the cursor and no later drain will ever see it. HealthKit’s own HKAnchoredObjectQuery is what solves this — it’s ordered by insertion and would fix deletions and edit-detection at the same time — and this release doesn’t use it. If your app has to be complete rather than merely current on iOS, do a full readSamples over the range you care about on a schedule of your own, and treat the subscription as a prompt to refresh rather than as the source of truth. Android is unaffected: change tokens are insertion-ordered already.

A busy window on iOS can hold more samples than one HealthKit query returns, and HealthKit offers no continuation token. When that happens the cursor advances only as far as the samples actually delivered and the batch reports hasMore(), so the next drain picks up the remainder rather than skipping it. Draining more often keeps each window small; a subscription left undrained for weeks will take several drains to catch up.

Handle isResyncRequired(). A Health Connect change token expires after 30 days and an iOS anchor can be rejected after a restore from backup; when that happens the batch carries no data and you must do a full time-range read to catch up.

A HealthBackgroundListener must be a public top-level class with a public no-argument constructor. Unlike some older callback APIs in this framework there is no need to do anything to keep it from being stripped: the build server scans for implementations and generates a factory that constructs each one with a direct new, so shrinking and obfuscation both follow the reference correctly.

54.7. Workouts

WorkoutManager workouts = Health.getInstance().getWorkouts();
workouts.startSession(new WorkoutConfiguration()
        .setActivityType(WorkoutActivityType.RUNNING)
        .setLocationType(WorkoutLocationType.OUTDOOR))
    .onResult((session, err) -> {
        if (err != null) {
            Log.e(err);
            return;
        }
        if (!session.isLive()) {
            // Android phones and iOS before 26: the clock and the saved
            // record are real, but nothing is collected unless you feed
            // it -- from a sensor, from location, or by hand.
            Log.p("Recording; connect a strap for heart rate");
        }
        session.start();
    });

Two capabilities that are easy to conflate and that this API keeps separate:

  • isLiveSessionSupported() — the OS runs a real session and keeps the app alive. False everywhere in this release. HKWorkoutSession exists on watchOS and on iOS 26 and later, and androidx.health.services exists on Wear OS, but neither is wired up yet, so every platform currently uses the recorded session described below. Branch on this query rather than on the OS version and your code will pick up native sessions when they land.

  • isSensorCollectionSupported() — the OS also gathers heart rate and energy into that session by itself. False everywhere in this release, for the same reason: the live session is what collects, and there isn’t one yet.

The second being false everywhere today, a workout records only what you feed it. On an Android phone that isn’t a degraded fallback: Health Connect has no live-session concept, and inserting a session, batching your own data and updating it at the end is precisely the flow Google documents. Feed it with addSamples(…​), or by attaching a Bluetooth sensor.

getStatistic(…​) returns null rather than a fabricated zero when nothing has been collected. Sessions aren’t restored after the process dies — a killed workout is over, end() was never called, and nothing is written.

Ending a workout tells you what the platform would not keep. WorkoutSample.WORKOUT_NOT_PERSISTED is set when the session record itself has no write form — neither mobile platform accepts one through this API — and WorkoutSample.SAMPLES_NOT_PERSISTED names the data types whose samples were refused, comma separated. Power, speed and both cadences are the ones that bite: they’re exactly what a bike or foot pod feeds in, and Health Connect has no single-value write form for any of them. Check both and upload what matters to you; the workout object itself is complete either way.

54.8. Bluetooth health sensors

HealthSensors sensors = Health.getInstance().getSensors();
if (!sensors.isSupported()) {
    return;
}
SensorScanSettings settings = new SensorScanSettings()
        .addProfile(HealthSensorProfile.HEART_RATE)
        .setTimeoutMillis(15000);

sensors.startScan(settings, new SensorDiscoveryListener() {
    public void sensorDiscovered(HealthSensor sensor) {
        sensors.connect(sensor, HealthSensorProfile.HEART_RATE,
                new SensorSessionOptions().setAutoReconnect(true))
            .onResult((session, err) -> {
                if (err != null) {
                    return;
                }
                session.addListener(new SensorSampleListener() {
                    public void sensorSample(SensorSession s,
                            HealthSample sample) {
                        Log.p("bpm: " + ((QuantitySample) sample)
                                .getValue(HealthUnit.COUNT_PER_MINUTE));
                    }
                    public void sensorStateChanged(SensorSession s,
                            SensorSessionState state) { }
                    public void sensorError(SensorSession s,
                            HealthException e) { }
                });
            });
    }
    public void scanFailed(HealthException e) {
        Log.e(e);
    }
});

com.codename1.health.sensors covers the adopted Bluetooth SIG profiles — Heart Rate, Cycling Power, Cycling and Running Speed & Cadence, Health Thermometer, Weight Scale, Blood Pressure and Glucose — so any conforming device works without per-vendor code.

This layer is built entirely on com.codename1.bluetooth.le and needs no platform health store, so it works identically everywhere Bluetooth LE does, including the desktop and JavaScript ports where no health store exists at all. It needs Bluetooth permissions, not health ones, and the build server makes the same distinction: an app that uses only this package isn’t treated as a health-data app and gets neither a HealthKit entitlement nor a Google Play health-permissions review.

Speed and cadence sensors transmit cumulative counters rather than rates, with counters and event timers that wrap every 32 or 64 seconds. The framework differences them for you; hand-rolling that arithmetic is the most commonly botched part of these profiles.

SensorSessionOptions.setWriteToStore(…​) defaults to false on purpose. When the OS is recording heart rate into HealthKit during a workout, a strap that also writes its own samples double-counts in every downstream average. Attach the session to the workout with setWorkoutSession(…​) instead; turn write-through on for standalone measurements the OS knows nothing about, such as a weight from a scale.

A heart-rate session reports the rate. RR intervals are decoded by HeartRateMeasurement but aren’t delivered as samples — they’re the input to an HRV calculation, not a measurement of it, and no platform has a data type that would store them. If you need HRV, subscribe to characteristic 0x2A37 through com.codename1.bluetooth.le and call HeartRateMeasurement.parse(…​) on the raw value yourself; both are public API.

Blood pressure is the exception: it’s two values in one reading, which HealthKit models as a correlation and Health Connect as its own record type, and neither is implemented in this release. A cuff reading routed to the store fails with TYPE_NOT_SUPPORTED on both phones rather than being dropped without a word; the local and simulator stores keep it fine. Read it off the session and persist it yourself for now.

54.9. Nutrition

com.codename1.health.nutrition logs food and drink as a sparse set of nutrient amounts. A logged apple sets four fields and a scanned packaged food might set thirty; both platforms model this as a record with several dozen optional fields, and NutritionSample keeps that sparseness explicit rather than exposing forty nullable getters. A nutrient that was never measured reads back as null, not zero — the same distinction aggregate buckets make, and one that matters to someone managing their sodium.

HealthDataType.NUTRITION is local and simulator only in this release. Neither the wire format nor either port’s type map knows the multi-nutrient record shape, so a read or write of it on a phone is refused with TYPE_NOT_SUPPORTED before it reaches the platform rather than being dropped without a word. Individual dietary quantities do reach the phones as ordinary quantity samples — HYDRATION on both, DIETARY_ENERGY on iOS — so use those when the number has to land in the user’s real health store.

54.10. Build configuration

Health data is the most regulated data these APIs touch, and the build is strict about it.

Table 12. iOS build hints
HintEffect

ios.NSHealthShareUsageDescription

Why your app reads health data. Required if you touch the store; the build fails without it.

ios.NSHealthUpdateUsageDescription

Why your app writes health data. Required if you write.

ios.health.backgroundDelivery

Adds the background-delivery entitlement. Inferred from observer usage.

ios.health.recalibrateEstimates

Adds the estimate-recalibration entitlement.

ios.health.required

Adds healthkit to UIRequiredDeviceCapabilities.

Table 13. Android build hints
HintEffect

android.health.read

Comma-separated data types to read. Required.

android.health.write

Comma-separated data types to write.

android.health.background

Adds READ_HEALTH_DATA_IN_BACKGROUND.

android.health.history

Adds READ_HEALTH_DATA_HISTORY.

android.health.privacyPolicyUrl

Your privacy policy. Required.

android.health.connectVersion

Overrides the Health Connect client version.

Codename One doesn’t inject a placeholder privacy string for health, unlike camera or Bluetooth. Apple reviews health purpose strings against what the app actually does, so a generic placeholder is precisely what gets an app rejected — it would not even achieve the "keeps the build working" goal. The build fails with a message naming the hint instead.

The Android data types can’t be inferred. A data type is referenced as a constant, which compiles to a field read, and the build server’s class scanner records only type and method references — so the permission set genuinely can’t be derived from your bytecode. Declaring it explicitly also matches Google Play policy, which requires you to request exactly what you use. Health Connect additionally raises minSdkVersion to 26 and requires targetSdkVersion 30 or higher.

HealthKit is entitlement-gated, unlike CoreBluetooth. The com.apple.developer.healthkit capability must be enabled for your App ID and present in the provisioning profile you build with, or the build fails at code-signing.

An app that only uses Health.getSensors() needs none of this. The sensor layer is built on com.codename1.bluetooth.le and touches no HealthKit, so the build injects neither the framework nor the entitlement nor the usage strings, and none of the hints above apply — it needs ios.NSBluetoothAlwaysUsageDescription instead. Reaching for the store is what pulls HealthKit in, and it’s getStore() rather than Health.getInstance() that reports a missing usage string, so the sensor-only path stays clear of the whole apparatus.

54.11. Privacy and store policy

Read this section before shipping. Both stores enforce policies that go well beyond the technical permissions.

Apple. HealthKit data may not be used for advertising or use-based data mining beyond health, fitness and medical research, and may not be shared with third parties without explicit consent for that specific sharing. A privacy policy URL in App Store Connect is mandatory — HealthKit apps are rejected without one. HealthKit data may not be stored in iCloud. Purpose strings must be specific: "Reads your step count to show weekly activity trends," not "Needs health access."

Google. Health Connect access is gated by the Play Console health apps declaration form, and each permission is approved individually. Your app must implement a privacy-policy activity that’s shown before the permission dialog — Codename One declares it for you, and this isn’t optional: Health Connect declines to show its consent dialog to an app that lacks one. Play’s Health Apps policy prohibits using the data for ads, for credit, insurance or employment decisions, or transferring it to a data broker.

What Codename One itself does. The framework never uploads health data anywhere. The simulator’s event log records data types and counts but never sample values. And the build fails rather than inventing a purpose string on your behalf.

Practical rules. Request the narrowest type set, at the moment of use rather than at launch. Never persist samples to unencrypted Storage. Treat the on-device store as the source of truth and avoid server round-trips. Delete health data on account deletion.

Finally, the read-authorization asymmetry is a privacy feature, not an obstacle. iOS hides read denial so that an app can’t infer what a user is concealing. Design your UI to respect that.

54.12. Simulating health

The simulator ships a scriptable health store under Simulate → Health, and every action is also callable from a test with CN.execute("health:itemN").

ActionEffect

Grant / Deny All Permissions

The permissive and fully denied cases

Grant Write, Deny Read Without Error

Reproduces the iOS trap exactly

Load Demo Dataset (7 days)

Synthetic steps, heart rate, sleep and weight

Emulate HealthKit / Health Connect Permissions

Switches read-denial behaviour

Make Health Unavailable

Emulates a missing provider

health:item10 / health:item11

Arm a one-shot query or save failure

Click "Grant Write, Deny Read Without Error" before you ship. It’s the default-adjacent case real users will produce and the one a permissive store never shows you: authorization appears to succeed, the status reads UNKNOWN, and every query comes back empty with no error. If your UI shows an error or accuses the user of denying access, it’s wrong.

The simulator’s data is synthetic and seeded, not recorded from anyone. That’s intentional: unlike Bluetooth traces, where the sensitive parts are identifiers that can be scrubbed, the sensitive part of a health sample is the value being asserted on, and a resting heart rate together with sleep timing and step cadence is quasi-identifying.

54.13. Troubleshooting

A query returns empty even though permission was granted. On iOS you can’t distinguish a denied read from an empty store — this is by design. Use hasAnyData(…​), show "no data available," rather than an error, and offer openHealthSettings(). Reproduce it in the simulator with "Grant Write, Deny Read Without Error."

The chart shows zero for days the user was active. You are substituting zero for a null aggregate. An empty bucket means no data, not no steps.

Totals are double. A phone and a watch are both writing the same type, on iOS as much as on Android — aggregation runs on raw samples in shared code, so HealthKit’s de-duplicating statistics engine never sees the query. Pin it with addSource(…​).

Background delivery never fires. No platform pushes health data to a closed app in this release — see the subscriptions section. Call drainChanges() from your background-fetch handler and branch on isPushDelivery() rather than assuming.

Replaying stored glucose records isn’t implemented. SensorSession.requestStoredRecords(…​) always fails with NOT_SUPPORTED in this release. Replay runs over the Record Access Control Point — a stateful protocol on a second characteristic — and shipping it untested against real meters would be worse than saying so. Live glucose notifications work normally.

Sample metadata doesn’t reach the phone’s health store. HealthSample.getMetadata() round-trips locally — the simulator, desktop and JavaScript store — but isn’t persisted to HealthKit or Health Connect in this release, so a sample written and read back on a device returns without it. Health Connect has no arbitrary metadata at all, only a single client record id. Keep your correlation identifier in your own storage.

Recorded workouts aren’t kept alive on Android. The manifest declares no foreground service, so a recorded workout lives exactly as long as your process does. If you need one to survive backgrounding, keep the app alive yourself.

BODY_MASS_INDEX is rejected on Android. Health Connect has no BMI permission or record — BMI is derived, not stored — so the build fails rather than requesting an unrelated body-composition permission on your behalf. Read BODY_MASS and HEIGHT and compute it. HealthKit does store BMI, so iOS is unaffected.

Deleting a sample by id does nothing. Health Connect deletes by record class plus id, so HealthDeleteRequest.byId(…​) takes the data type alongside the identifier. Naming the wrong type deletes nothing and still reports success.

The weekly chart is off by a day twice a year. You are bucketing by a fixed 24 hours across a daylight-saving transition. Use HealthInterval.calendarDays(1, tz).

The iOS build fails complaining about a usage description. That’s intentional — add ios.NSHealthShareUsageDescription with text describing what your app actually does with the data.

The Health Connect permission dialog never appears. Health Connect requires the permissions-rationale activity, which Codename One declares only when it detects health usage. Check that the build log mentions the health manifest fragments.

55. Smart Home

Codename One ships a cross-platform smart-home API under com.codename1.home that reads the accessories in a user’s home, reads and writes what they can do, watches them for change, runs scenes, and adds new Matter accessories.

SmartHome.getInstance() is the single entry point and never returns null: ports without smart-home support return a fallback whose operations fail fast with HomeError.NOT_SUPPORTED and whose graph accessors return empty lists, so calling code needs no platform-specific if.

Every callback — AsyncResource results, change batches, structure events — is delivered on the EDT, on every platform. That includes the desktop, the simulator and the JavaScript port, which marshal rather than answering on whichever thread called, so a read started from a worker thread can update the UI from its callback directly.

55.1. The model

A HomeStructure holds rooms (HomeRoom) and accessories (Accessory). An accessory has one or more services (AccessoryService) — a two-gang wall switch is one accessory with two services — and a service exposes traits (Trait).

A trait is a canonical capability, not a platform identifier. Trait.BRIGHTNESS is the same constant whether the accessory is behind HomeKit or Matter, and the port maps it to HMCharacteristicTypeBrightness or to Level Control’s CurrentLevel for you. No HomeKit characteristic string and no Matter cluster id ever reaches your code.

Graph objects are immutable snapshots. Nothing on an Accessory calls into the platform when you read a getter; when the topology moves, a HomeStructureListener tells you to call refresh() and read the graph again once it completes.

CapabilityiOS / iPadOSwatchOS / tvOSAndroid (default)Android (Home APIs)Simulator / desktop

Read the accessory graph

yes

yes

no

yes

yes (synthetic)

Read and write traits

yes

yes

no

yes

yes

Run scenes

yes

yes

no

yes

yes

Create scenes

yes

yes

no

varies

yes

Live change delivery

yes (foreground)

yes (foreground)

no

no

no

Add a Matter accessory

yes

no

yes

yes

yes (synthetic)

Zones

yes

yes

no

no

no

Automations and triggers

no

no

no

no

no

Branch through the capability queries — SmartHome.getAvailability(), AccessoryService.supports(…​), TraitSubscription.isPushDelivery(), Commissioner.isSupported() — rather than through platform detection.

55.2. Quick start

SmartHome home = SmartHome.getInstance();
HomeAvailability availability = home.getAvailability();
if (availability == HomeAvailability.PROVIDER_NOT_INSTALLED
        || availability == HomeAvailability.PROVIDER_UPDATE_REQUIRED) {
    home.openProviderSetup();
    return;
}
if (availability == HomeAvailability.NOT_CONFIGURED) {
    // Authorized, and the user has never set up a home. Sending them
    // to the ecosystem app is the only way forward.
    home.openEcosystemApp();
    return;
}
if (availability == HomeAvailability.COMMISSIONING_ONLY) {
    // The ordinary Android answer: accessories can be ADDED and not
    // read. Say so rather than rendering an empty house.
    Log.p("smart home is add-only on this build");
}
home.refresh().onResult((structures, err) -> {
    if (err != null) {
        Log.e(err);
        return;
    }
    HomeStructure h = home.getPrimaryStructure();
    if (h == null) {
        return;
    }
    for (Accessory a : h.getAccessoriesSupporting(Trait.ON_OFF)) {
        cnt.add(new Label(a.getName()));
    }
});

Check getAvailability() before anything else. Several of its states are recoverable by the user, and the recovery differs: sending someone to Google Play for a provider update when what they need is to open the Home app helps nobody.

55.3. Android’s default answer isn’t "available"

This is the one thing worth understanding before you design a screen.

With no extra setup, an Android app can commission a Matter accessory into the user’s Google Home and can do nothing else. The graph is empty and no trait can be read or written. getAvailability() reports COMMISSIONING_ONLY, and that state exists so the word AVAILABLE doesn’t mean something different on Android than it does on iOS.

Reading and controlling accessories on Android needs the Google Home APIs, which need a Google Cloud project and a Google Home Developer Console registration carrying the SHA-1 of your own signing key. Codename One can’t create those on your behalf. That integration isn’t in this release; commissioning is.

SmartHome.getConfigurationProblems() explains the gap in text aimed at you rather than at your user — log it or show it in a debug screen, and don’t put it in front of somebody holding a phone.

55.4. Reading and writing

A read returns one TraitReading per requested trait, and a partial success is the normal case: three values and one unreachable accessory is a successful read with one failed row, not a failed operation.

TraitReadRequest request = new TraitReadRequest()
        .add(thermostat, thermostatService, Trait.CURRENT_TEMPERATURE)
        .add(thermostat, thermostatService, Trait.CURRENT_HUMIDITY);

SmartHome.getInstance().read(request).onResult((readings, err) -> {
    if (err != null) {
        Log.e(err);
        return;
    }
    for (TraitReading reading : readings) {
        if (reading.isFailed()) {
            label.setText("unavailable");
        } else if (!reading.hasValue()) {
            // Not an error, and not zero: the accessory has nothing to
            // report yet. Rendering this as 0 degrees is the bug this
            // check exists to prevent.
            label.setText("no reading yet");
        } else if (reading.getTrait() == Trait.CURRENT_TEMPERATURE) {
            label.setText(reading.getValue()
                    .getDouble(TraitUnit.CELSIUS) + " C");
        }
    }
});

Note the three-way branch. A reading can carry a value, carry an error, or carry neither — and the third is the one that catches people. An accessory can legitimately have nothing to report: a sensor that hasn’t measured yet, a light in white mode that has no meaningful hue. Nothing in this API substitutes a zero for a measurement that was never taken, so hasValue() has to be asked.

Writing is the mirror image, and a batch write partly succeeding is equally normal — "turn off every light" against a home with one dead bulb mostly worked.

AccessoryService svc = lamp.getPrimaryService();
TraitConstraint dimming = svc.getConstraint(Trait.BRIGHTNESS);

// The accessory's own range, not the trait's nominal 0..100. A slider
// built from the wrong one offers values the lamp refuses -- and a
// refused write is an error rather than a silent clamp.
Slider slider = new Slider();
slider.setMinValue((int) dimming.getMinimum());
slider.setMaxValue((int) dimming.getMaximum());

SmartHome.getInstance()
        .write(new TraitWrite(lamp, svc, Trait.BRIGHTNESS,
                TraitValue.of(slider.getProgress(), TraitUnit.PERCENT)))
        .onResult((result, err) -> {
            if (err != null) {
                Log.e(err);
            } else if (!result.isApplied()) {
                Log.p("the lamp refused: " + result.getError());
            }
        });

Build controls from the accessory’s own TraitConstraint rather than from the trait’s nominal range. A dimmer whose floor is 10 percent says so there, and writing below the floor is refused rather than clamped: an app that asked for 5 percent and got 10 percent without being told never learns it was wrong.

55.5. Values and units

TraitValue is one type with kind-checked getters rather than a class per trait. There’s no zero-argument getDouble() on purpose: getDouble(TraitUnit) makes you name the unit you expect and refuses a cross-dimension request, which is what stops a Celsius setpoint being rendered as Fahrenheit.

Everything proportional is normalized to a percentage, so Matter’s 0-254 level control and 0-10000 covering position never reach your code. Color temperature is in mireds, because that’s what both platforms use natively — and remember that a higher mired value is a warmer light. TraitValue.getColorTemperatureKelvin() converts if you would rather think in Kelvin.

Where a mapping is lossy, TraitValue.getRawPlatformValue() carries the platform’s own ordinal alongside the canonical one. Air quality has six levels on HomeKit and seven on Matter; the canonical enum follows Matter and the raw value is there for an app that needs to be exact.

55.6. Watching for change

Nothing wakes your app for an accessory change. HomeKit delivers changes only while your app is running in the foreground, and the Google Home APIs need a live signed-in client — the home hub, not your app, is what reacts to a sensor while the phone is asleep.

TraitReadRequest resync = new TraitReadRequest()
        .add(lamp, lamp.getPrimaryService(), Trait.ON_OFF)
        .add(lamp, lamp.getPrimaryService(), Trait.BRIGHTNESS);
SubscriptionRequest request = new SubscriptionRequest()
        .add(lamp, lamp.getPrimaryService(), Trait.ON_OFF)
        .add(lamp, lamp.getPrimaryService(), Trait.BRIGHTNESS)
        .setDeliverInitialValues(true);

TraitSubscription subscription = SmartHome.getInstance()
        .subscribe(request, batch -> {
            if (batch.isResyncRequired()) {
                // Changes were missed, so everything not in this batch
                // is stale. Ignoring this leaves a screen showing
                // values from before the gap, indefinitely.
                SmartHome.getInstance().read(resync);
            }
            for (TraitReading reading : batch.getReadings()) {
                label.setText(String.valueOf(reading.getValue()));
            }
        });

if (!subscription.isPushDelivery()) {
    // Everywhere except HomeKit in the foreground. Without a drain the
    // listener never fires, which looks exactly like a sensor that
    // never triggers.
    SmartHome.getInstance().drainChanges();
}

Ask isPushDelivery() rather than assuming. Where it answers false, which is everywhere except HomeKit in the foreground, the listener fires only when you call drainChanges(). Wire that into the point where your app comes to the foreground.

Within a subscription’s window, changes are coalesced per accessory, service and trait, keeping only the newest value — so dragging a dimmer produces one reading showing where it ended up rather than forty showing the journey. A batch is a state update, not an event log.

Hold on to the TraitSubscription and stop it. A dropped one keeps its listener reachable and keeps the platform delivering.

55.7. Scenes

A scene is a named set of accessory states that can be applied in one go: HomeKit’s action set, a Google Home scene. TraitWrite.toSceneAction() turns the changes a user just made into one.

Automations — a scene plus a trigger — aren’t supported. HomeKit, Google Home and Matter model triggers in three incompatible ways and Matter has none, so there’s no honest common shape. SmartHome.isAutomationSupported() answers false everywhere, so an app can say why the feature it wanted isn’t offered.

55.8. Adding an accessory

Commissioner commissioner = SmartHome.getInstance().getCommissioner();
if (!commissioner.isSupported()) {
    commissioner.openEcosystemApp();
    return;
}

// Validated here, in your own words, rather than failing inside the
// operating system's sheet with wording about the app.
SetupPayload payload;
try {
    payload = SetupPayload.parse(scannedCode);
} catch (IllegalArgumentException badCode) {
    label.setText(badCode.getMessage());
    return;
}

commissioner.commission(new CommissioningRequest()
                .setSetupPayload(payload)
                .setSuggestedName("Kettle"))
        .onResult((result, err) -> {
            if (err != null) {
                Log.e(err);
                return;
            }
            if (result.wasCommissionedToThisApp()) {
                SmartHome.getInstance().refresh();
            } else {
                // Added to the user's home, and this app cannot
                // address it. Say that, rather than showing an empty
                // device screen.
                label.setText("Added " + result.getAccessoryName()
                        + " to your home");
            }
        });

Two things about this flow.

The interaction isn’t yours. Both mobile backends hand it to an operating-system sheet, and the user may be several minutes — they have to power the accessory on, hold a button, sometimes join it to Wi-Fi. There’s no progress reporting. Don’t put a short timeout or a determinate progress bar behind it.

Success doesn’t always mean you got a device. wasCommissionedToThisApp() is the difference between "the accessory was added to the user’s home" and "your app can address it." On Android with Play services alone the answer is the former: you’re told the name and nothing more. On iOS it’s the former too — Apple’s sheet reports that the flow finished without saying what was added or which home it went to — so refresh the graph and look at what’s new. An app that assumes otherwise shows a "your new device" screen with nothing on it.

SetupPayload.parse() validates a scanned QR or typed manual code in pure Java before any of that starts, so a mis-scanned sticker gets a message in your wording rather than an opaque failure inside the OS sheet.

55.9. Build hints

# iOS: required as soon as your app touches accessories. The build FAILS
# without it, deliberately -- iOS terminates an app that reaches HomeKit with
# no usage description, and Codename One will not invent privacy copy in your
# name.
codename1.arg.ios.NSHomeKitUsageDescription=Controls the lights and locks in your home

# Optional. Opt out of the generated Matter add-device extension target.
codename1.arg.ios.home.commissioning=false

# Optional. The app group the extension and the app share; defaults to
# group.<your.package.name>.
codename1.arg.ios.home.appGroup=group.com.example.myapp

# Optional. Refuse to install on a device without HomeKit.
codename1.arg.ios.home.required=true

Everything else is automatic, and automatic in both directions: an app that never references com.codename1.home gets no HomeKit framework, no entitlement, no Play services dependency and no extra Xcode target. The build server decides by scanning your bytecode.

Two consequences worth knowing:

  • The com.apple.developer.homekit entitlement is added only when your app actually touches accessories. An app that only calls getAvailability() links HomeKit and gets no entitlement — which matters, because that entitlement has to be enabled on your App ID and signing without it fails. If your app does touch accessories, enable HomeKit for your App ID in the Apple developer console and regenerate your provisioning profile; the build checks and tells you if you haven’t.

  • Commissioning is separated because on iOS it costs an entire generated app-extension target. That’s why com.codename1.home.commissioning is its own package: referencing the package is what asks for the extension.

55.10. Previewing in the simulator

The simulator, the desktop ports and the JavaScript port run a local simulated house rather than reporting themselves unsupported. It’s awkward rather than tidy on purpose — a two-gang switch, a bridged pair of lights behind a hub, an unreachable socket, a thermostat in auto mode where TARGET_TEMPERATURE genuinely has no value, and a dimmer with a real floor — because those are the shapes that break naive code.

It also doesn’t push changes and never completes an operation inline, matching what a real backend does. Code written against a store that answers instantly races the moment it meets one that doesn’t.

55.11. Not claimed in this release

Stated plainly, because each of these has a query that answers rather than a silence:

  • Automations, triggers and conditions. Scenes only.

  • Background accessory events. Nothing wakes your app; see isPushDelivery().

  • Topology writes. Creating homes, renaming rooms, moving accessories between rooms, inviting users. The graph is read-only.

  • Cameras and video. A camera is visible in the graph and there’s no stream, snapshot or recording API.

  • Security systems and alarm panels. Matter has none in shipping releases, and a half-working alarm API is a safety problem rather than a feature gap.

  • Matter events. Only attributes, which is why LockState.JAMMED is unreachable outside HomeKit — a jam is an event on Matter and an attribute on HomeKit.

  • Energy, appliance and diagnostic clusters.

  • The Google Home APIs accessory graph on Android. See above.

And one structural point: by default Codename One isn’t a Matter controller. There’s no fabric of its own, no direct on-network commissioning and no Thread border-router interaction. Everything Matter goes through the operating system’s ecosystem, so the Apple Home or Google Home app has to be installed and set up.

55.12. Commissioning onto your own fabric

CommissioningRequest.setCommissionToThisApp(true) asks for one thing more: that the accessory also join a Matter fabric your app owns, as a second administrator beside the user’s home. On iOS the generated commissioning extension then carries Apple’s own Matter stack, a fabric key it keeps in the keychain and the controller storage that fabric persists through — so the build sees the call and switches that machinery on. Set codename1.arg.ios.home.commissioning.fabric=true if your call is somewhere the class scanner can’t see, such as behind reflection, and codename1.arg.ios.home.commissioning.vendorId to your own Matter vendor id — the default is the test vendor 0xFFF1, which some accessories refuse.

Apple’s Matter framework starts at iOS 16.4, so an own-fabric extension is built for 16.4 while your app keeps the 16.1 floor MatterSupport asks for. On 16.1 through 16.3 the extension can’t load, and Commissioner.getStyle() answers ECOSYSTEM_APP_HANDOFF there rather than OS_OWNED_UI — the accessory is added in the Home app, exactly as in a build with no extension. Ask for the style before you offer the button and that difference is invisible to your user.

Read the generated CN1MatterSetup/RequestHandler.swift before you turn it on. The implementation ships in every commissioning build, commented out, precisely so that what you enable is code you have read.

What it doesn’t buy you is a way to talk to the accessory over that fabric: Codename One has no API for that yet, and reads and writes still go through the ecosystem. What it buys is that the accessory is already commissioned when such an API arrives, and that CommissioningResult.wasCommissionedToThisApp() can be true. Android’s Play Services commissioning can’t do it at all, and ignores the request.

56. Printing

Codename One can hand a document to the platform printing system through the com.codename1.printing package. The API shows the native print dialog where the user picks a printer, adjusts options and confirms the job. It works on iOS (AirPrint), Android, desktop builds, the simulator, Windows and the JavaScript port.

The document is a file in FileSystemStorage identified by its path and mime type. Every printing platform accepts PDF (application/pdf) and common image types (image/png, image/jpeg).

56.1. Quick start: Print a PDF

Printer.printPDF(path, listener) is shorthand for Printer.print(path, "application/pdf", listener). Use the general print variant for other document types, for example Printer.print(photoPath, "image/jpeg", listener).

Gate the call on isPrintingSupported(). When it returns false the listener receives a FAILED result without any UI appearing, so an unguarded call does no harm, but a well-behaved app hides or disables its print button instead.

56.2. Printing images

Printer.printImage(image, listener) prints a com.codename1.ui.Image directly. The image is encoded to a temporary PNG file behind the scenes and the file is deleted once the print flow finishes:

Image chart = renderChart();   // your own drawing code
Printer.printImage(chart, result -> {
    if (result.isFailed()) {
        ToastBar.showErrorMessage("Print failed: " + result.getError());
    }
});

This pairs well with drawing on a mutable image, so you can compose a printable page (a receipt, a ticket, a chart) with the standard Graphics API and send it to paper without generating a PDF.

56.3. Handling the result

The listener receives a PrintResult on the EDT, exactly once per request. The listener may be null for fire-and-forget printing. There are three statuses:

  • COMPLETED – the job was handed to the platform printing system. Most platforms can’t observe the physical printer, so this means "queued/sent," not "paper came out."

  • CANCELLED – the user dismissed the print dialog without printing, on platforms that can observe the dialog outcome.

  • FAILED – the job couldn’t start or the printing system reported an error. getError() may carry a short platform message.

Cancel detection is best-effort. iOS, Windows, PDF printing on Android and image printing in the simulator report CANCELLED reliably. Image printing on Android, PDF hand-off on the desktop and the browsers report COMPLETED once the job is posed, since their printing pipelines don’t expose the user’s choice.

56.4. Permissions and the user gate

Printing requires no permission, entitlement, manifest entry or build hint on any platform. This is safe because printing is always user-confirmed: the API can only pose the native print dialog, and nothing reaches a printer until the user approves the job there. In that sense it follows the same trust model as the share API.

Silent printing - sending a job to a preselected printer without showing a dialog - isn’t supported by design.

Device policy can disable printing entirely, for example a managed Android work profile with the printing restriction or an iOS MDM profile that blocks AirPrint. Those cases surface as isPrintingSupported() returning false or as a FAILED result, so the result-handling code above covers them.

56.5. Platform notes

  • iOS – UIPrintInteractionController presents the AirPrint sheet; on the iPad it appears as a popover. The completion handler reports completed, cancelled or failed.

  • Android – the android.print framework. PDFs stream through a PrintDocumentAdapter; images go through the support library PrintHelper, which reports completion but can’t observe cancellation. Printing requires a foreground activity, so calls from background mode fail.

  • Desktop / simulator – images print through java.awt.print.PrinterJob with the native dialog; PDFs are handed to the operating system’s default PDF print flow.

  • Windows (native port) – the Win32 print dialog with GDI rendering; PDF pages render through the Windows.Data.Pdf API at the printer’s resolution.

  • JavaScript – the document loads into a hidden frame and prints through the browser’s print dialog. Browsers don’t report whether the user printed or cancelled, and PDF behavior varies with the browser’s built-in viewer.

56.6. Trying it in the simulator

The PrinterSample in the samples project demonstrates both flows: it prints a generated image and downloads a small PDF to print. On the desktop the native print dialog of your operating system appears, which makes it a convenient way to test result handling - print to a file or preview instead of paper.

The application surface is two annotations plus a tiny Navigation class — five static methods and one value type.

57.1. 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.

57.2. Path syntax

PatternMatchesBound

/about

/about

 — 

/users/:id

/users/42

id = "42"

/files/*

/files/photo.png (one segment)

* = "photo.png"

/files/**

/files/a/b/c (catch-all)

* = "a/b/c"

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.

57.3. 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.

57.4. 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:

  • @Route declared on a class that doesn’t extend Form

  • Pattern with no leading / or empty value

  • Path variable with no matching @RouteParam on the constructor / factory

  • Duplicate pattern declared on two different targets

  • @Route on an abstract class or non-public static method

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

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.

57.7. Testing

57.7.1. iOS Simulator

xcrun simctl openurl booted "https://example.com/users/42"

57.7.2. Android Emulator

adb shell am start -a android.intent.action.VIEW \
    -d "https://example.com/users/42" com.example.app

57.7.3. Desktop Simulator

Pass --cn1-arg=https://example.com/users/42 on the run command, or use the simulator’s URL-injection helper.

57.8. 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.

58. State Restoration and Continuity

An app that’s put in the background isn’t paused. Android reclaims the process routinely, iOS terminates a suspended app whenever it needs the memory, and in both cases what comes back isn’t the app the user left — it’s a fresh launch that happens to be wearing the same icon. The user sees their work replaced by the home screen and has no idea why.

com.codename1.continuity saves what the user was doing and brings it back. On Apple platforms it does one thing more: it offers that same work to the other devices the person is signed in to, so a draft begun on the phone can be finished on the iPad they pick up.

The two halves cost different things, so they’re two packages. com.codename1.continuity holds the framework and everything that carries work to a device the user is holding. com.codename1.continuity.sync holds a small key/value store the platform keeps in step across their devices, and referencing it earns an iOS build an entitlement. An app that wants the first shouldn’t have to arrange the second.

CapabilityiOS and macOSAndroidSimulator and desktopJavaScript

Restore after the process is killed

yes

yes

yes

yes

Restore the @Route screen stack

yes

yes

yes

yes

Carry on where they left off, on a device they’re holding

yes

 — 

simulated

 — 

A key/value store synced across devices

yes (com.codename1.continuity.sync)

 — 

simulated

 — 

Carry state to any other device

your StateRelay

your StateRelay

your StateRelay

your StateRelay

Branch on the capability queries — Continuity.isSupported(), Continuity.isContinuationSupported(), SyncedStore.isSupported() — rather than on platform detection. The first row is the one that matters most and it’s supported everywhere, because it’s pure storage with no platform behind it.

Every callback in this family arrives on the EDT.

58.1. Six Things Worth Knowing Before You Design Around This

Nothing happens until you ask for it. An app that never references this package behaves exactly as it always did, and so does one that references it and never calls Continuity.setStateProvider or Continuity.enable. Continuity.restore() is never called for you either. Where restoration belongs in a launch is a decision only the app can make, and a framework that guessed would be wrong for the apps that care most.

The route stack is free; everything else is yours. If your screens are declared with @Route, the framework already knows the navigation stack and restores it with no code from you. If your app navigates with new MyForm().show(), those moves aren’t addressable and there’s nothing to write down — so restore() hands your payload to the StateProvider and answers false, leaving you to show a screen. Both are supported; only the first is automatic.

Saving happens continuously, not at shutdown. Every navigation marks the state dirty and a checkpoint is written once per pass of the event loop, so by the time the operating system suspends the app the work is already done. Don’t look for a place to save on exit; there isn’t one worth using. Android blocks its own main thread until your stop() returns, so an app that did its saving there would pay for it on every suspend. Call Continuity.checkpoint() after changing something your provider reports that no navigation touched.

A payload has to survive leaving the device. It’s written to disk, handed to an operating system, and possibly delivered to a different device running a different build of your app — so it admits only String, Integer, Long, Double, Boolean, and List and Map of those. Anything else is refused where you produced it, with a message naming the key, rather than becoming a value that stops arriving on the other end with nothing to say so.

Codename One runs no relay server. Continuation between Apple devices is the platform’s; anything else — iPhone to Android, two devices that are never in the same room — goes through a StateRelay, which is your endpoint. That isn’t a gap to be filled later. Deciding which saved states belong to the same person is your account system’s question, and a framework that answered it would be guessing about your users.

A continuation isn’t secure storage. What you put in the payload crosses to another device and is held by the operating system on the way. Tokens, keys and anything you would not want restored on a device that merely shares an account belong in com.codename1.security.SecureStorage, with the payload carrying at most an identifier that means nothing on its own.

58.2. Saving and restoring

What restore returns for routed and hand-shown screens

Two pieces. A StateProvider supplies the half the framework can’t work out — the scroll position, the half-typed message, the record being edited — and installing one turns the framework on:

public void init(Object context) {
    Continuity.setStateProvider(new StateProvider() {
        public Map<String, Object> saveState() {
            Map<String, Object> state = new HashMap<String, Object>();
            state.put("draft", draftField.getText());
            return state;
        }

        public void restoreState(Map<String, Object> state) {
            draftField.setText((String) state.get("draft"));
        }
    });
}

Then start() reads as "restore, or else begin":

public void start() {
    if (!Continuity.restore()) {
        Navigation.navigate("/home");
    }
}

restore() returns true when it put a screen up, so the caller knows not to show its own. It returns false when there was nothing to restore and when the state carried no routes — the payload-only case above — which is why the fallback branch belongs there rather than behind a null check.

restoreState runs before the restored screens are built, so a form the route table is about to construct can read what the provider stashed while that form is being built.

Take a checkpoint by hand after a change no navigation followed:

public void onDraftSaved() {
    Continuity.setTitle("Draft to Dana");
    Continuity.checkpoint();
}

The title is what a receiving device may show the user before they accept, so it should name the work rather than the screen — Draft to Dana, not Compose.

By default a saved state never expires, because an app the user opens after a month should still come back where they left it. Where coming back is only meaningful for a while — a checkout, a booking hold, a queue position — say so:

public void expireACheckout() {
    Continuity.setMaxAge(15 * 60 * 1000);
}

58.3. Continuing on another device

Where a continuity payload travels and what it may carry

Nothing extra is required for the Apple case. Every checkpoint advertises the current state, and a device the user is holding is offered it by the system. What you may want is to say so in the interface, which is what the capability query is for:

public void describeWhatThisDeviceCanDo() {
    if (Continuity.isContinuationSupported()) {
        showBanner("Open this app on your other device to carry on there.");
    }
}

An arriving state is restored automatically. When moving the user is a decision your app should make — they’re midway through a payment, or the state belongs to a different account than the one signed in here — take it yourself. A listener that returns false has consumed the state: nothing is restored and no other listener is asked, which is what makes holding it and asking work:

public void askBeforeMovingTheUser() {
    Continuity.setAutoRestore(false);
    Continuity.addContinuationListener(new ContinuityListener() {
        public boolean stateReceived(AppState state) {
            held = state;
            if (Dialog.show("Continue?", "Pick up \"" + state.getTitle()
                    + "\" from your other device?", "Continue", "Stay here")) {
                Continuity.restore(held);
            } else {
                // Declining is a decision, and it has to be recorded. Returning false alone
                // only suppresses the state for THIS run -- false also means "keep it, I will
                // prompt again later" -- so without this the relay's unchanged document asks
                // the same question after every relaunch.
                Continuity.acknowledge(state);
            }
            // Consumed either way: the decision has been made here, and recorded either way.
            return false;
        }
    });
}

A state this device published is never offered back to its own listener, and a state already acted on is never acted on twice — a continuation and a relay routinely carry the same one.

58.4. Reaching every other device

A StateRelay is your endpoint, and RestStateRelay covers the common case:

public void useMyOwnEndpoint() {
    Continuity.setRelay(new RestStateRelay("https://api.example.com/continuity") {
        @Override
        protected String getToken() {
            return session.getAccessToken();
        }
    });
}

Two requests against the one URL. A POST carries the state as a JSON body, which you store against the signed-in user, replacing whatever you held for them. A GET answers with the newest state you hold for that user, or an empty body when you hold none. The JSON is a closed shape: your endpoint stores and returns the document and never needs to look inside it.

The token comes from getToken() rather than from the constructor because it’s read at every request, so a session that refreshes its token is followed with no further code.

A relay is written to when the app checkpoints and read only when something asks, so ask when the app comes back to the foreground:

public void onAppResumed() {
    Continuity.pollRelay();
}

On Android that call is already made for you when the activity resumes. Making it yourself as well is harmless — a state already seen is ignored.

Put Continuity.clear() and Continuity.disable() on your logout path, and Continuity.enable() on your login path. The advertised activity outlives your app’s own screen, so an account’s work would otherwise stay on offer to the devices around it after the user signed out — and anything still queued for the relay would have gone out later under the next account’s credentials, because a relay reads its token when the request runs:

public void onLogout() {
    // Both, and in this order. clear() forgets the account's data; disable() closes the
    // door behind it. clear() on its own leaves continuity ON, so a continuation that
    // arrives while your login screen is up is a valid arrival to a framework that is
    // still listening -- and the signed-out account's routes and payload get restored
    // over it.
    Continuity.clear();
    Continuity.disable();
}

public void onLogin() {
    // And open it again. Continuity stays off until you say otherwise, which is what
    // makes the gap above safe.
    Continuity.enable();
}
clear() alone isn’t a logout. It forgets the account’s data — the stored checkpoint, the advertised activity, anything queued for the relay — and it leaves continuity switched on by design, because forgetting state and turning the feature off are two different things and an app is entitled to do the first without the second. That means a continuation arriving while your login screen is up reaches a framework that’s still listening, and the signed-out account’s routes and payload are restored over it. disable() is what closes that gap, and enable() at login is what reopens it.

58.5. The synced store

com.codename1.continuity.sync.SyncedStore is the slow, patient half: a handful of durable choices — which theme, which sort order, which tutorial they already dismissed — kept in step across the devices one person is signed in to, without those devices ever being near each other.

public String readSortOrder() {
    return SyncedStore.get("sortOrder", "byName");
}

public void writeSortOrder(String order) {
    if (!SyncedStore.put("sortOrder", order)) {
        // No synced store here, or it is full. The value still has to live somewhere, so
        // fall back to this device's own preferences rather than losing the choice.
        com.codename1.io.Preferences.set("sortOrder", order);
    }
}

Note the shape: a read always has a default, and a write reports whether the store actually took the value — checked by reading it back, not assumed. That isn’t defensive style, it’s the API being honest. The store is empty on a device that has never synced, the user can switch the whole mechanism off, and it exists on Apple platforms only — so a synced value with a local default behind it makes the design work on every platform.

Changes made elsewhere arrive without values, on every platform that has such a store at all, so re-read what your screen shows rather than assuming you know which key moved:

public void followTheStore() {
    SyncedStore.addChangeListener(new SyncedStoreListener() {
        public void storeChanged() {
            // No values are carried, on any platform. Re-read what this screen shows.
            applySortOrder(SyncedStore.get("sortOrder", "byName"));
        }
    });
}

This isn’t storage. It’s small, the platform decides when to sync it, and nothing in it should be anything your app can’t do without.

58.6. Developing Without Hardware

The simulator carries a simulated continuity platform, so all this works on the desktop — and the Simulate → Continuity menu scripts the cases that are otherwise reachable only with two devices in your hands. Every item is also callable from a test with CN.execute("continuity:itemN").

Continue Here (As Another Device) hands whatever the app is currently advertising straight back to it. That’s the whole feature in one click. If it does nothing, the app hasn’t taken a checkpoint yet — which is itself the answer to the question of why nothing is being offered.

The rest reproduce traps rather than the happy path:

  • Continue A Route This Build Dropped. A screen goes away in a rebuild and the states already sitting on the user’s other devices still name it. The restore survives on the frames it can still build.

  • Continue With No Routes (Payload Only). What an app that doesn’t use @Route produces. An app that assumed restore() always shows something finds out here.

  • Continue Something From Yesterday. Exercises setMaxAge, and the listener that has to decide whether moving the user somewhere they were yesterday is a courtesy or an ambush.

  • Change The Synced Store Elsewhere. The notification carries no values, so an app that re-reads only the key it assumed changed reads a stale one.

  • Make The Synced Store Unsupported and Make Continuation Unsupported. What every non-Apple platform reports. An app that put a required setting in the synced store and never checked isSupported() loses it here, with no error, exactly as it would on Android.

58.7. Build Hints

HintDefaultWhat it does

ios.continuity.sync

unset

Whether this project wants the iCloud key-value store. Left unset the build decides from the bytecode. Set false to drop the entitlement; set true to declare it, which is what lets the signing preflight check your profile before the build is sent.

Everything is automatic by default. Referencing com.codename1.continuity compiles the NSUserActivity handling into the iOS build and declares this app’s activity type in NSUserActivityTypes, which is what lets another device be offered the work — iOS continues an activity only when the app declared its type, so an app that skipped this would publish states nobody is ever shown. Referencing com.codename1.continuity.sync additionally asks for the com.apple.developer.ubiquity-kvstore-identifier entitlement. Apps that touch neither package get none of it, on any platform. Android needs nothing injected at all: no permission, no manifest entry, no dependency.

The activity type is your package name followed by .continuity, derived the same way in the build and at runtime, so there’s nothing to configure and nothing to get out of step. Continuity.getActivityType() returns it, which is the first thing to check when a continuation never arrives.

That entitlement is the one part of this that can stop a build. Apple grants it only through an App ID with the iCloud capability enabled, so a profile issued before that was switched on matches your bundle id and authorizes none of it — and the build fails at codesigning, talking about an entitlement rather than about the capability. Codename One checks the profile before sending the build and warns, naming both ways out: enable iCloud on the App ID and regenerate the profile, or drop the entitlement.

codename1.arg.ios.continuity.sync=false

With that set, SyncedStore.isSupported() reports false at runtime and the rest of the app is unaffected. Handing work to a nearby device needs no entitlement and keeps working either way.

A restored state is restored on a device that has the app, not necessarily on the device that saved it and not necessarily by the person who did. Treat the payload as a description of what screen to show, never as proof of who is looking at it: re-check the signed-in account after restoring, and put nothing in a payload that would be a disclosure if it appeared on a family member’s iPad. This is also what makes Continuity.clear() on logout more than housekeeping.

59. App Intents: Siri, Spotlight and Shortcuts

Siri, Spotlight, the Shortcuts app, an Android launcher shortcut, a button on a home-screen widget and a large language model are all asking your application the same question: what can you do, and will you do it now? Codename One models them as one concept — an app intent — under the com.codename1.intents package. You declare a capability once and the framework projects it to each of them.

59.1. Three nouns

An intent is a named, titled, invokable capability. It takes parameters, which are either primitives or entities — your application’s own nouns, which the platform can list, search and ask the user to choose between before your code runs. It produces an IntentResult: a value, a line to speak, a snippet to show, a route to open, or any combination of those.

59.2. Declaring an intent

A handler is a public static method carrying @AppIntent:

@AppIntent(value = "log_workout", title = "Log a workout",
        description = "Records a completed workout",
        phrases = {"Log a workout in ${applicationName}"},
        headless = true, timeoutSeconds = 5)
public static IntentResult logWorkout(
        @IntentParam(value = "kind", title = "What kind of workout?",
                     options = {"run", "ride", "swim"}) String kind,
        @IntentParam(value = "minutes", title = "How many minutes?") int minutes) {
    WorkoutStore.append(kind, minutes);
    return IntentResult.spoken("Logged a " + minutes + " minute " + kind + ".");
}

The method is static for a reason that matters on a device rather than as a style preference: the build generates a direct static call to it, and a direct call is the only form that survives the iOS translator’s dead-code elimination and Android’s obfuscation. It also makes the contract visible — a handler can be asked to run in a process that exists only to answer it, where no instance of your class has been constructed and there is no user interface.

At build time the Codename One Maven plugin scans your compiled bytecode, validates every declaration, and generates both the reflection-free dispatch table and the native declarations each platform compiles into the app. A malformed declaration fails the build rather than going quiet on a device.

Spoken phrases carry three rules, which Apple treats as halting errors that produce no App Intents metadata at all. The build checks them for you, so the message names your declaration instead of arriving as an opaque failure from the metadata processor:

  • Every phrase must contain ${applicationName}.

  • A phrase may reference at most one parameter. Write one phrase per parameter rather than combining them.

  • A phrase parameter must be an entity. A primitive can’t appear in a phrase — which costs little, because leaving it out still has the platform ask for it, using the title you gave its @IntentParam.

59.3. Intents and routes are different questions

A @Route answers "given this URL, which screen?" and hands back a Form. An intent answers "given this verb and these arguments, what happens and what comes back?" and hands back a value. They can’t be merged: a deep link has no return channel, always foregrounds, is stringly typed, and has no way to express "ask the user which playlist first."

They meet at exactly one point — an intent that opens the app is a deep link — so that’s the one place they’re joined. Declare opensRoute and the framework builds the URL from the bound parameters and navigates through your existing route table:

@AppIntent(value = "show_order", title = "Show order", opensRoute = "/orders/{orderId}")
public static IntentResult showOrder(@IntentParam("orderId") String orderId) {
    return IntentResult.opens("/orders/" + orderId);
}

One screen, one route pattern, two ways in. The build checks that opensRoute matches a @Route you actually declared.

59.4. Entities: The part that makes intents feel intelligent

An entity-typed parameter is what lets the platform run its own picker before your code is ever called. "Play a playlist" with nothing specified makes the system ask "Which one?" then fill the list from your suggested query, and hand your handler the chosen object. A plain String parameter can never do that — it can only be typed or spoken verbatim.

@IntentEntity(value = "playlist", title = "Playlist")
public static class Playlist {
    private String id;
    private String name;

    @EntityId
    public String getId() {
        return id;
    }

    @EntityTitle
    public String getName() {
        return name;
    }

    @EntityQuery(EntityQuery.Kind.BY_ID)
    public static Playlist byId(String id) {
        return Library.playlist(id);
    }

    @EntityQuery(EntityQuery.Kind.SUGGESTED)
    public static java.util.List<Playlist> recent() {
        return Library.recentPlaylists();
    }

    @EntityQuery(EntityQuery.Kind.SEARCH)
    public static java.util.List<Playlist> matching(String query) {
        return Library.search(query);
    }
}

Your class stays your class: there is no interface to implement and no base class to extend, because a domain model outlives the framework it’s used with. The build reads the annotated members and generates the adapter.

A BY_ID query is mandatory. An entity crosses the platform boundary as its id and nothing else, so resolving that id back to an object is the one lookup the framework can’t do without.

Write a query the way you’d write a headless handler: static, reading persisted state, depending on nothing your init() set up. The system can ask for a picker while it’s still launching your application, and it blocks on the answer, so a query is called at a moment when your own startup may not have run yet — and a query that returns nothing then produces a picker with nothing in it. The framework and its storage are fully up by that point; your application’s own singletons aren’t.

Entity ids outlive your process and often your release. The device search index and the system’s donated shortcuts both persist your id and hand it back later, so an id derived from a list position or a content hash starts resolving to the wrong object after your data changes, with no error reported anywhere.

Indexing is the half of this feature most applications will get the most from, and it costs the least:

// After a sync, off the EDT
Intents.index(recentOrders);

// In init(), so a tap that cold-started the app is delivered as soon as possible
Intents.setSelectionHandler(entity -> Navigation.navigate("/orders/" + entity.getId()));

Remove entries when the content behind them goes away. An index entry outlives the data it describes, so deleted content keeps appearing in device search and taps resolve to nothing until the application removes it:

Intents.removeFromIndex("order", orderId);

59.6. The headless contract

A headless intent can run in a process the system started only to answer it, with nothing on screen. That’s what lets an assistant answer without your app appearing, and it’s a real constraint rather than a flag.

The default is false, and that costs something worth knowing: an intent that doesn’t declare headless brings the app to the foreground when it runs, so there’s a window for it to act on. That isn’t permission to touch one directly. No handler runs on the event dispatch thread, foreground or not — see below — so a foreground handler reaches the user interface the way every other background thread in Codename One does, through Display.callSerially. What foregrounding buys is that there’s something on screen for that call to land on, and somewhere for a route to open. Declaring headless = true is what buys the answer-in-place behaviour — and what obliges you to earn it.

A headless handler may use Storage, Preferences, Database, NetworkManager, Log, surfaces publishing and Intents.index. It must not touch Form, Dialog, Component, Display.getCurrent(), the camera, capture, or anything else that needs a window: there is no window. It must also not call Display.callSeriallyAndWait, which can deadlock against a foregrounded event dispatch thread.

A platform-dispatched invocation never runs its handler on the event dispatch thread, whatever the platform and whatever the source. One can arrive while your app is visible — a widget button, a search result tapped on a running app — and a handler blocking the event thread there is a visible freeze.

Intents.invoke is the exception, by construction: it’s synchronous, so it runs the handler on whatever thread called it and hands the result back to that caller. Call it from the event dispatch thread and you block the UI for as long as the handler runs, exactly as any other synchronous call from there would — pass it to Display.startThread if the work isn’t instant.

Every invocation carries a deadline, and IntentContext.isCancelled() flips when it passes. Cancellation is cooperative: nothing interrupts a running handler, and anything a platform invocation returns afterward is discarded, so commit durable work to storage as you go rather than only at the end. Intents.invoke is the one exception, by design: you are blocked in that call waiting for the result and nothing else will report it, so a late one is still returned to you rather than thrown away.

The platform will usually allow around twenty seconds. Don’t use them. A spoken interaction that takes ten seconds has already failed as an interaction. Aim under two, and for anything genuinely slower return IntentResult.opens(…​) and do the work in the app where there’s a UI to show progress in.

59.7. What each platform actually does

CapabilityiOSAndroidSimulator

Declared intent

App Intent and App Shortcut

launcher shortcut

Intents window row

Voice invocation

Siri, from your phrases

none

typed into the window

Headless execution

background launch of the app

service, no Activity

in process

Open a route

native, then your route table

trampoline, then your route table

shows the form

Entity parameters

system picker and search

passed by id

picker from your real queries

System disambiguation

yes

no — the app foregrounds its own picker

in-app picker

Content indexing

Core Spotlight

long-lived launcher shortcuts

searchable list

Donation

learned suggestions

dynamic shortcuts

logged

Spoken result

spoken aloud

toast, headless runs only

shown as text

Snippet

rendered natively

not shown

rasterized preview

Returned value

piped to the next action

caller only, not the launcher

shown

The last three rows are where Android differs most, so they’re worth stating outright. A launcher shortcut is a launch: it starts something, and nothing waits for an answer, so there is no channel to hand a result back through. A spoken line is shown as a toast, and only when the run was headless — that being the one case with no UI of its own to say it in. A snippet isn’t rendered at all; no Android surface displays one for a shortcut, so a handler that returns one isn’t failing, its snippet simply has no consumer there. A returned value goes to whoever called Intents.invoke, which is how an app uses its own intents as a command layer and works on every port, but it doesn’t travel back to the launcher, because nothing there asked a question.

A handler whose entire result is a snippet therefore does nothing observable when it’s started from an Android launcher shortcut. Write handlers that also change state, or that name a route with opensRoute, and the outcome is visible everywhere.

An intent marked destructive is absent from parts of that table by design: it’s not published as a launcher shortcut and not donated, because both run on a single tap with nothing between the tap and the action, and confirmation is the whole point of the flag. It stays available through the assistant and the Shortcuts app, which confirm first.

Android isn’t Siri parity, and this framework doesn’t pretend otherwise. Android has no contract by which an assistant invokes an application capability and receives a typed result back, so phrases and system disambiguation are iOS-only. Branch on Intents.isVoiceInvocationSupported(), not on areIntentsSupported(), when deciding whether to tell a user they can talk to your app.

What Android does deliver is real: launcher shortcuts, dynamic shortcuts the system learns to suggest from donation, and genuinely headless execution — the last of which is actually easier there than on iOS, because the Android port already boots without an Activity for background fetch.

59.8. The floor everything else sits on

An intent is guaranteed only to be invokable with its parameters supplied, and to produce a value. Everything else — phrases, pickers, speech, snippets, indexing, donation — is an enhancement some platform may not offer. Write each handler so it’s still correct when nothing arrives but its parameters and nothing is consumed but its return value, and it will behave the same everywhere.

Because dispatch is generated code rather than a platform service, Intents.invoke(…​) works on every port, including those with no intent support at all. An application can use its own intents as an internal command layer and get the platform integration as a bonus where it exists.

59.9. Testing without a device

The simulator’s Simulate → App Intents window lists every intent your application declares, generates a parameter form from the declaration, fills entity pickers from your real query methods, and runs the intent through the same entry point the iOS and Android ports call. Nothing in it shortcuts the framework, so a bug in marshalling, coercion or the deadline shows up on your desktop rather than on a device.

59.10. What this costs your iOS deployment target

Nothing, unless you declare an intent — and possibly nothing even then.

Indexing and donation run on Core Spotlight and NSUserActivity, which are Objective-C and have been available since long before the framework’s current minimum, so an application that only publishes content to device search never changes its deployment target.

Declaring an @AppIntent brings in Apple’s App Intents framework, which needs a newer minimum. Every generated type is availability-guarded, so the intended outcome is that your target is untouched and the intents simply don’t appear on older devices. Where a toolchain refuses that, the build raises the floor only for applications that declare an intent. Two build hints keep the decision yours:

HintEffect

ios.intents.minDeploymentTarget

A floor a declared intent contributes. Empty by default, so declaring an intent raises nothing: Xcode emits App Intents metadata below the deployment target, and the generated declarations are availability-fenced, so they’re simply not offered on devices too old to run them. Set it to 16.0 if you would rather not ship a build whose intents are inert for part of your audience.

ios.intents.appIntents

false keeps Spotlight indexing and shortcut donation while suppressing App Intents generation entirely.

If you’ve pinned a deployment target below what a declared intent needs, the build says so and stops rather than moving your pin or dropping your intents behind your back.

59.11. Zero cost when unused

Referencing this package is what makes the build inject the native plumbing. An application that never touches com.codename1.intents gets none of it — no frameworks, no shortcut resources, no manifest entries, no deployment-target change — and builds exactly as it did before.

60. Document Provider

An app’s documents normally live inside it. The user opens your app to reach them, and no other app can. A document provider changes that: the content you publish appears as a location in the system file browser - the Files app on iOS, the storage picker on Android. It can be browsed, previewed and opened by other apps without your app being launched at all.

Codename One generates the platform plumbing for you from build hints. There is no native code in your project on any platform.

60.1. When you need this

If all you want is for the files your app already writes to be visible in the Files app under the device’s own heading, you need no extension and none of this chapter. Two plist keys do it:

codename1.arg.ios.plistInject=<key>UIFileSharingEnabled</key><true/>\
<key>LSSupportsOpeningDocumentsInPlace</key><true/>

That exposes your app’s own documents directory as-is. Use the document provider when you want something the directory listing can’t give you: a tree that doesn’t match your storage layout, content fetched from a server on demand, names and types that differ from the file names, or a location that appears even when the device holds none of the content yet.

60.2. How it works

On iOS the file browser doesn’t talk to your app. It talks to an app extension - a separate process the system starts on demand, which runs while your app is dead and can’t call your Java code. That single fact shapes the whole API.

You don’t answer questions from the browser; you publish: a tree of DocumentNode objects is serialized into a container both processes can read, and the generated extension serves the browser from it. Publish whenever your data changes - after a sync, after a login - not in response to being browsed, because you won’t be asked.

Android has no such split; the provider runs inside your app. The API is the same there anyway, so one publishing discipline works everywhere.

60.3. Publishing a tree

Build a tree and publish it. Each file node names its content in one of two ways.

From the shared directory. Write the bytes under getSharedDirectory() and give the node a relative path. Nothing else is involved - no server, no network code - and the content opens instantly because it’s already on the device.

String shared = DocumentProvider.getSharedDirectory();
DocumentNode root = DocumentNode.folder("root", "My Invoices");
for (Invoice invoice : invoices) {
    String name = invoice.title + ".pdf";
    writeInto(shared + "/" + name, invoice.pdf);
    root.add(DocumentNode.file(invoice.id, name)
            .setContentType("application/pdf")
            .setPath(name)
            .setSize(invoice.pdf.length)
            .setLastModified(invoice.modified));
}
DocumentProvider.publish(root);

On iOS the shared directory is inside the App Group container, not your app’s own sandbox. A file written to FileSystemStorage’s home directory is not visible to the extension. Always resolve paths against `getSharedDirectory().

Update published content by writing a new file and republishing the tree against it, never by rewriting a file already in the published tree. Another app can be reading a published file at any moment, and rewriting one in place refills the same file underneath that reader, which sees the two versions spliced together. Publish the replacement under a new name and call publish again: the readers already open finish the old file, and the ones that come after open the new one.

From your server. Give the node a remoteId and tell the extension where to fetch it. The index can then list far more than the device holds, which is what a cloud drive wants.

DocumentProvider.setRemoteEndpoint("https://api.example.com/drive", sessionToken());
DocumentNode root = DocumentNode.folder("root", "Example Drive");
for (Invoice invoice : invoices) {
    root.add(DocumentNode.file(invoice.id, invoice.title + ".pdf")
            .setContentType("application/pdf")
            .setRemoteId(invoice.serverKey)
            .setSize(invoice.pdf.length)
            // The date, not the size, is what tells the browser the content changed: a
            // correction that keeps the length would move nothing otherwise.
            .setLastModified(invoice.modified));
}
DocumentProvider.publish(root);

A node may carry both. The local copy always wins, which is how a cached document opens without a round trip.

The location is read-only. Your app is the only writer, so the browser offers no editing, renaming or deleting and never presents a save that would fail. To change what people see, publish again.

Folders nest as you would expect:

DocumentNode root = DocumentNode.folder("root", "My Invoices");
DocumentNode year = DocumentNode.folder("y2031", "2031");
year.add(DocumentNode.file("inv-1", "January.pdf")
        .setContentType("application/pdf")
        .setPath("2031/january.pdf"));
root.add(year);
DocumentProvider.publish(root);

Node ids must be stable for the lifetime of the item. The platform remembers them (a favourite in the Files app, a recent document, a running download), so reusing an id for different content, or renumbering ids on every publish, makes the browser point at the wrong thing. Derive the id from your own record key, never from list order.

Call clear() on logout. The published content outlives your process, so documents left behind stay browsable by whoever holds the device:

DocumentProvider.clear();

isSupported() tells you whether this platform can show the location, which is worth checking before offering a setting for it. Everything else is a safe no-op where the feature is unavailable, so no other platform guards are needed:

if (DocumentProvider.isSupported()) {
    showSetting("Show my documents in Files");
}

60.4. Build hints

Referencing com.codename1.documents is enough to generate the extension. These hints configure it:

codename1.arg.ios.documentProvider.enabled=true
codename1.arg.ios.documentProvider.appGroup=group.com.example.myapp

ios.documentProvider.enabled is redundant for the build itself - the build detects the reference - but the Certificate Wizard and the signing preflight can’t read your bytecode, so setting the hint tells them to create and check the extension’s signing assets. The wizard sets it for you.

The App Group defaults to group.<your package>. It must start with group., which Apple requires, and it’s the entire transport between your app and the extension: without it the location appears empty.

Optional:

codename1.arg.ios.documentProvider.displayName=Example Drive
codename1.arg.ios.documentProvider.deploymentTarget=16.0

displayName names the location in the browser and defaults to your app’s display name. deploymentTarget defaults to 16.0. Below 16.0 Codename One generates the older NSFileProviderExtension instead, which Apple has deprecated; the two claim the same extension point, so a build gets one or the other and never both. Your app’s own deployment target is unaffected either way - the extension simply never runs on older systems.

Set ios.documentProvider.extension=false to skip the whole iOS lowering and leave the API an inert no-op. Xcode build settings for the extension target can be overridden with ios.documentProvider.buildSettings.SETTING=value.

Android needs no configuration. The provider is declared in the manifest with the authority <your package>.documents when the app references the API.

60.5. The remote endpoint

When a node with a remoteId is opened and no local copy exists, the reader issues:

GET <endpoint>/fetch?id=<remoteId>
Authorization: Bearer <token>

The reader expects the file’s bytes with status 200. Anything else is reported to the user as a failure to open the document. The token is the one you passed to setRemoteEndpoint.

The request is made by the extension, not by your app, so it must succeed without any state your app holds in memory - the token is all it carries. Publish a fresh token whenever you renew one; the reader picks up the latest value each time it runs.

This is what the published index looks like on disk, which is worth knowing when you are working out why a location looks wrong:

{
  "id": "root",
  "name": "Example Drive",
  "folder": true,
  "children": [
    {
      "id": "inv-2031-01",
      "name": "January.pdf",
      "folder": false,
      "contentType": "application/pdf",
      "remoteId": "invoices/2031/01",
      "size": 48213,
      "lastModified": 1735689600000
    }
  ]
}

60.6. Signing

Apple signs an app extension against its own App ID. Your app’s profile covers your app’s bundle id and nothing beneath it, so the generated CN1Documents target needs an App ID of its own - <your package>.CN1Documents - with the same App Group, and its own provisioning profile.

The Certificate Wizard does every step: it creates the App Group, enables it on both App IDs, creates the extension’s App ID, generates an App Store profile and a development profile for it, downloads them and writes them into your project settings. That’s the recommended route.

To do it by hand, create the App ID and profiles in the Apple Developer portal and name them:

codename1.ios.appext.CN1Documents.provision=${user.home}/certs/CN1Documents_dist.mobileprovision
codename1.ios.debug.appext.CN1Documents.provision=${user.home}/certs/CN1Documents_dev.mobileprovision

The unqualified codename1.ios.appext.CN1Documents.provision is the fallback for both build types. You can also host the profile and name the URL in ios.appext.CN1Documents.provisioningURL.

Supply nothing and Xcode fails minutes into a cloud build with:

Provisioning profile "MyApp_Distribution" has app ID "com.example.myapp", which doesn't
match the bundle ID "com.example.myapp.CN1Documents"

Codename One refuses that build before sending it, naming the extension and the bundle id its App ID has to match.

A wildcard App ID (com.example.*) isn’t a way around this, unlike for other extensions. It covers the bundle id, and it still can’t sign this extension: the extension declares the App Group it shares with the app, and Apple doesn’t offer the App Groups capability on a wildcard App ID at all. Signing fails on the entitlement rather than on the name, so the build is refused here too. The app’s own profile has the same problem, because it carries the same group. Both need explicit App IDs - which is what the Certificate Wizard creates.

A Mac build embeds no extension and needs no signing assets for one: Apple’s FileProvider framework is unavailable to Mac Catalyst apps, so the Mac slice of an iOS project can’t host a provider however it’s entitled. isSupported() answers false there and every publish call is a no-op, so the same code runs unchanged.

A local "ios-source" build needs none of this either: you sign the generated Xcode project in Xcode as usual.

60.7. Testing

isSupported() answers false in the simulator, and every publish call is a safe no-op there, so your code needs no guards beyond the one shown above. The simulator still writes everything a device would, under cn1documents in the app’s home directory, so you can read back exactly what a device would have been handed.

On a device, publish a tree and open the Files app: the location appears under "Browse" alongside iCloud Drive. On Android it appears in the storage picker. A local "ios-source" build produces the CN1Documents target in the generated Xcode project, so you can confirm the extension compiles and is embedded before signing anything.

61. Apple Wallet Extension

Card issuers (banks, fintechs) can let users add a payment card to Apple Wallet from inside the Wallet app itself. The user opens Wallet, taps the add button and sees the issuer’s cards listed under "From apps on your iPhone" - without ever launching the issuer’s app. Apple implements this through a pair of app extensions called issuer provisioning extensions, and Codename One can generate them for you from build hints with no native code in your project.

This is distinct from the in-app flow where your app shows an "Add to Apple Wallet" button. The extension flow starts inside the Wallet app, runs in a separate process and usually runs while your app isn’t running at all.

61.1. How the extension works

Apple defines two extension points:

  • A non-UI extension that Wallet queries for status and the list of available cards, and that produces the encrypted provisioning payload when the user adds a card. Wallet gives it 100ms to answer the status query, so it can’t perform network calls or heavy work there.

  • An optional authorization UI extension that presents a login screen inside Wallet when the non-UI extension reports that authentication is required.

Because the extensions run in a separate process, they can’t call into your Java code. The Codename One integration bridges this in two ways:

  1. Your app pre-publishes the card list, card art and an auth token through com.codename1.payment.WalletExtension. The data lands in a shared App Group container where the generated extension reads it instantly.

  2. The one step that genuinely needs the issuer’s backend - producing encryptedPassData from the certificates and nonce that Apple hands the extension - is performed by an HTTPS endpoint you host. The generated extension POSTs the request there and relays the response to Wallet.

61.2. Prerequisites from Apple

Before any of this works you need approvals that only Apple and your card network can grant:

  • The com.apple.developer.payment-pass-provisioning entitlement is restricted. Apple grants it per app on request (see the In-App Provisioning documentation on the Apple developer site). After approval, enable it under the App ID’s Additional Capabilities tab for the app and for each extension App ID.

  • Each extension needs its own App ID (for example com.mybank.app.WalletNonUIExtension) and its own provisioning profile carrying the entitlement.

  • The card network/PNO pass metadata must list the extension App IDs in associatedApplicationIdentifiers, otherwise Wallet never invokes the extensions.

  • The extensions require iOS 14. The generated extension targets set their own deployment target, so your app can still target an older iOS version.

61.3. Mode 1: Generate the extensions from build hints

Add these build hints to the iOS build:

codename1.arg.ios.wallet.extension=true
codename1.arg.ios.wallet.appGroup=group.com.mybank.app
codename1.arg.ios.wallet.issuerEndpoint=https://api.mybank.com/wallet/provision

That generates the non-UI extension, wires it into the Xcode project as an embedded extension target and injects the App Group into the app and extension entitlements. To also generate the login UI extension add:

codename1.arg.ios.wallet.includeUI=true
codename1.arg.ios.wallet.authEndpoint=https://api.mybank.com/wallet/login

In Java, publish the user’s cards whenever they change (typically after login) and keep a fresh token published so Wallet can skip the login screen:

if (WalletExtension.isSupported()) {
    WalletExtension.setPassEntries(new WalletPassEntry[] {
        new WalletPassEntry()
            .identifier(card.getPrimaryAccountIdentifier())
            .title("My Bank Debit Card")
            .cardholderName(user.getFullName())
            .primaryAccountSuffix(card.getLast4())
            .paymentNetwork("Visa")
            .localizedDescription("My Bank Debit Card")
            .artPng(cardArt.getImageData())
    });
    WalletExtension.setRemotePassEntries(sameEntries); // Apple Watch list
    WalletExtension.setAuthToken(session.getToken());
    WalletExtension.setRequiresAuthentication(false);
}

Call WalletExtension.clear() on logout. The card art must be a PNG without personally identifiable information (Apple requires square corners and no full card number). The extension automatically filters out cards that are already provisioned on the device or the paired watch.

61.3.1. The issuer endpoint

When the user adds a card, the generated extension POSTs JSON to the ios.wallet.issuerEndpoint URL:

{
  "certificates": ["base64...", "base64..."],
  "nonce": "base64...",
  "nonceSignature": "base64...",
  "cardIdentifier": "the WalletPassEntry identifier",
  "authToken": "the token published via setAuthToken"
}

The token is also sent as an Authorization: Bearer header. Your backend performs the network-specific encryption (this always happens server side - the keys never live on the device) and responds with:

{
  "activationData": "base64...",
  "encryptedPassData": "base64...",
  "ephemeralPublicKey": "base64..."
}

For the RSA_V2 scheme return wrappedKey instead of ephemeralPublicKey.

61.3.2. The login UI extension

When you can’t guarantee a fresh token, report setRequiresAuthentication(true) and enable the UI extension. It shows a minimal username/password form inside Wallet, POSTs {"username", "password"} to the ios.wallet.authEndpoint URL, expects {"token"} back and stores it where the non-UI extension picks it up. If the generated form doesn’t fit your brand or login model, use the injection hints below or bring your own extension (Mode 2).

61.3.3. Customizing the generated code

The generated Objective-C contains marker comments at every interesting point, and each marker has a matching build hint that injects your snippet there: ios.wallet.nonuiImportsInject, ios.wallet.statusInject, ios.wallet.passEntriesInject, ios.wallet.remotePassEntriesInject, ios.wallet.generateRequestInject (runs before the POST - the mutable payload dictionary is in scope, so you can add fields your backend expects), ios.wallet.generateResponseInject, ios.wallet.uiImportsInject, ios.wallet.uiViewDidLoadInject, ios.wallet.uiAuthRequestInject and ios.wallet.uiAuthResponseInject.

Xcode build settings of the extension targets can be overridden with ios.wallet.nonui.buildSettings.SETTING=value and ios.wallet.ui.buildSettings.SETTING=value - useful in local builds for DEVELOPMENT_TEAM or CODE_SIGN_STYLE.

61.3.4. Provisioning profiles for cloud builds

Cloud device builds sign each extension with its own profile. Either place the .mobileprovision files under common/src/main/resources and name them in build hints:

codename1.arg.ios.wallet.nonuiProvisioningProfile=WalletNonUI.mobileprovision
codename1.arg.ios.wallet.uiProvisioningProfile=WalletUI.mobileprovision

Alternatively, host them at URLs the build server can reach and supply ios.wallet.nonuiProvisioningURL / ios.wallet.uiProvisioningURL instead. Profiles contain no private keys, so bundling them in resources is safe; the build keeps them out of the final app bundle. The build validates that each profile matches your distribution certificate and actually carries the payment-pass-provisioning entitlement, and fails with an actionable message when it doesn’t.

In local builds ("ios-source" target) no profile hints are needed - you sign the generated Xcode project in Xcode as usual.

61.4. Mode 2: Bring your own extension

If you already have Xcode extension targets - from an issuer SDK vendor or an existing native app - skip the ios.wallet.* hints and drop each extension into the generic app extension mechanism instead. Create ios/app_extensions/MyWalletExtension/ in your project containing:

  • The extension’s source files (.m, .h, .swift)

  • Info.plist with the NSExtension dictionary

  • MyWalletExtension.entitlements

  • Optional buildSettings.properties with Xcode build settings, one per line (set IPHONEOS_DEPLOYMENT_TARGET=14.0 for Wallet extensions)

  • The extension’s own .mobileprovision for cloud device builds - see below

Each folder becomes an embedded extension target with bundle id <your package>.<folder name>. This mechanism isn’t Wallet-specific - it embeds any iOS app extension type.

61.4.1. Signing a brought-in extension

Apple signs an app extension against its own App ID. The app’s provisioning profile covers the app’s bundle id and nothing beneath it, so every extension needs its own App ID and its own profile - two extra of each for a Wallet pair. Supply none and the extension target inherits the app’s profile, and Xcode fails the build with:

Provisioning profile "MyApp_Distribution" has app ID "com.mybank.app", which does not
match the bundle ID "com.mybank.app.WalletNonUIExtension"

Codename One refuses that build before it’s sent, naming the extension and the bundle id its App ID has to match. To fix it, create an App ID for <your package>.<folder name> in the Apple Developer portal - for a Wallet extension with the com.apple.developer.payment-pass-provisioning entitlement Apple grants on request, and with the same App Group as the app - generate a provisioning profile for it against the same certificate as the app, and supply it in one of three ways:

  1. Place the .mobileprovision file inside ios/app_extensions/MyWalletExtension/. It’s picked up automatically and kept out of the app bundle. Profiles carry no private keys, so committing one is safe.

  2. Point at it from codenameone_settings.properties, which suits a profile that lives outside the repository:

    codename1.ios.debug.appext.MyWalletExtension.provision=${user.home}/certs/MyWalletExtension_dev.mobileprovision
    codename1.ios.release.appext.MyWalletExtension.provision=${user.home}/certs/MyWalletExtension_dist.mobileprovision

    The unqualified codename1.ios.appext.MyWalletExtension.provision is the fallback for both build types.

  3. Host it and name the URL in ios.appext.MyWalletExtension.provisioningURL (or the ios.debug.appext…​ / ios.release.appext…​ variants).

The build validates the profile it’s given: it must match the app’s signing certificate, cover the extension’s bundle id, be current, and grant the App Groups and the payment-pass-provisioning entitlement the extension’s .entitlements asks for. Each of those fails with a message naming the extension, rather than as an Xcode code signing error minutes into the build.

A wildcard App ID (com.mybank.*) is the one case that needs no extra profile - it’s already covering the extension bundle ids, and the build lets it through. Wallet extensions can’t use one: the payment-pass-provisioning entitlement is granted per explicit App ID.

Local "ios-source" builds need none of this - you sign the generated Xcode project in Xcode as usual.

61.5. Testing

Wallet extensions can’t be exercised in the iOS simulator’s Wallet app; testing the full flow requires a device, a provisioning profile with the entitlement and a card network sandbox. What you can verify earlier:

  • A local "ios-source" build with the hints produces the WalletNonUIExtension/WalletUIExtension targets - open the generated Xcode project and build.

  • WalletExtension.isSupported() returns false in the simulator and on other platforms, and all publish calls are safe no-ops there, so the Java code needs no platform guards beyond the one shown above.

62. Miscellaneous features

62.1. Phone functions

Most of the low-level phone functionality is accessible in the Display class. Think of it as a global class that covers access to the "system."

62.1.1. SMS

Codename One supports sending SMS messages but not receiving them as this functionality isn’t portable. You can send an SMS using either the Display singleton or the static helper methods on CN. Both APIs throw IOException, which you should handle in case the native layer reports a failure:

try {
    // true opens the platform composer, which is what both Android and
    // iOS offer. iOS opens it either way, but Android takes the flag
    // literally and its background path is not implemented
    Display.getInstance().sendSMS("+999999999", "My SMS Message", true);
    // Or: CN.sendSMS("+999999999", "My SMS Message", true);
} catch(IOException err) {
    Log.e(err);
    Dialog.show("SMS Failed", "Unable to send the SMS", "OK", null);
}

Both Android and iOS launch the native SMS application with your message composed, for the user to send. Neither sends in the background, so check getSMSSupport rather than assuming — the constant is the contract, and the base implementation defaults to SMS_SEAMLESS.

That’s why the samples pass true for the interactive argument. On Android the two-argument overload does nothing at all — it asks for the seamless send, and that branch isn’t implemented. The iOS port ignores the flag and opens the composer either way, so passing true is what works on both.

The getSMSSupport API returns one of the following options:

  • SMS_NOT_SUPPORTED - for desktop, tablet etc.

  • SMS_SEAMLESS - sendSMS won’t show a UI and will send in the background

  • SMS_INTERACTIVE - sendSMS will show an SMS sending UI

  • SMS_BOTH - sendSMS can support both seamless and interactive mode

The sendSMS can accept an interactive argument: sendSMS(String phoneNumber, String message, boolean interactive)

Pass true for it whenever getSMSSupport reports SMS_INTERACTIVE. The two-argument overload asks for a seamless send, and a port that only offers the system composer does nothing with that request — on Android nothing happens at all, because the background-send path isn’t implemented there. Both the Android and the iOS port report SMS_INTERACTIVE. A platform reporting SMS_SEAMLESS sends without a composer, and one reporting SMS_BOTH does whichever the interactive argument asks for, which is why the switch above branches on the constant instead of hard coding one call.

A typical use of this API would be something like this:

try {
    switch(Display.getInstance().getSMSSupport()) {
        case Display.SMS_NOT_SUPPORTED:
            return;
        case Display.SMS_SEAMLESS:
            showUIDialogToEditMessageData();
            Display.getInstance().sendSMS(phone, data);
            return;
        default:
            // interactive ports hand off to the platform composer. On
            // Android the two-argument overload asks for a seamless
            // send it does not implement, and nothing happens at all
            Display.getInstance().sendSMS(phone, data, true);
            return;
    }
} catch(IOException err) {
    Log.e(err);
    Dialog.show("SMS Failed", "Unable to send the SMS", "OK", null);
}

62.1.2. Dialing

Dialing the phone is pretty trivial. This should open the dialer UI without physically dialing the phone, as that’s discouraged by device vendors.

You can dial the phone by using:

Display.getInstance().dial("+999999999");

62.1.3. Call detection

Codename One includes a generic call detection API through Display.isCallDetectionSupported() and Display.isInCall().

This API is intentionally best effort and should be used as a UX hint (for example, to defer a non-critical animation while the app is interrupted).

On iOS, isInCall() is inferred from app interruption lifecycle events, which means it can report true for non-call interruptions (for example: Control Center, app switching, permission sheets), and some call flows may still be missed.

Because iOS toggles this flag during app lifecycle transitions, polling it with a UITimer can miss the interruption window entirely (timers are paused while the app is inactive, and the flag is reset when the app becomes active again). If you need to react, check it from lifecycle callbacks such as your app’s stop()/start() flow instead of periodic polling.

On Android, call detection is unsupported because robust detection would require invasive telephony permissions that are intentionally avoided:

Display display = Display.getInstance();
if (display.isCallDetectionSupported() && display.isInCall()) {
    Log.p("Call interruption is currently active (heuristic)");
}

62.1.4. E-Mail

You can send an email through the platforms native email client with code such as this:

Message m = new Message("Body of message");
Display.getInstance().sendMessage(new String[] {"[email protected]"}, "Subject of message", m);

You can add one attachment by using setAttachment and setAttachmentMimeType.

You need to use files from FileSystemStorage and NOT Storage files!

You can add more than one attachment by putting them directly into the attachment map for example:

Message m = new Message("Body of message");
m.getAttachments().put(textAttachmentUri, "text/plain");
m.getAttachments().put(imageAttachmentUri, "image/png");
Display.getInstance().sendMessage(new String[] {"[email protected]"}, "Subject of message", m);
Some features such as attachments etc. Don’t work in the simulator but should work on iOS/Android

62.2. Larger text accessibility

Codename One can adapt to the operating system’s larger text accessibility setting. The Display APIs expose two signals: isLargerTextEnabled() indicates whether the user has enabled larger text, and getLargerTextScale() returns the scale factor that should be applied to fonts.

If you want the theme system to automatically scale its fonts when larger text is enabled, enable the UIManager setting:

UIManager.getInstance().setUseLargerTextScale(true);

You can also enable this behavior in the theme by setting the useLargerTextScaleBool theme constant to true.

Component fonts are the theme’s job — don’t scale them by hand. For anything the theme can’t reach, a custom-painted row or an icon measured in code, read the values directly:

void snippet() throws Exception {
    Display display = Display.getInstance();
    // 1.0 when the user has not asked for larger text, so the same
    // arithmetic works either way
    float scale = display.isLargerTextEnabled() ? display.getLargerTextScale() : 1f;

    // a custom-painted row has no Style for the theme to scale, so give it
    // the same preference the theme fonts get
    int rowHeight = Math.round(baseRowHeight * scale);
    int iconSize = Math.round(baseIconSize * scale);
    drawingArea.setPreferredH(rowHeight);
    g.drawRect(0, 0, iconSize, iconSize);
}

62.2.1. How the scale is computed per platform

  • iOS: the scale is the ratio between the body text size at the user’s current Dynamic Type setting and the iOS standard 17pt body size, that is, [UIFont preferredFontForTextStyle:UIFontTextStyleBody].pointSize / [UIFont systemFontSize]. Scales for the standard slider stops range from 0.82× (Extra Small) through 1.00× (Large, default) up to 1.35× (Extra Extra Large). When the user enables Larger Accessibility Sizes in Settings → Accessibility → Display & Text Size → Larger Text, the slider extends with five more stops whose scales reach as high as 3.12×. These values match what every native iOS app sees from UIFontMetrics.

  • Android: the scale is Configuration.fontScale, which is bucketed by Android (typically 0.85, 1.0, 1.15, 1.30).

Because the scale is multiplicative, a base font size that’s already larger than the platform default will compound. For example, a defaultFontSizeInt: 18 in your CSS is 6% larger than iOS’s 17pt body, so at iOS Dynamic Type Extra Extra Large (1.35×) your text will render at 24.3pt versus a native iOS app’s 23pt. If you intentionally use a base size larger than the platform default and you also enable useLargerTextScaleBool, expect the absolute size at high accessibility settings to be visibly larger than typical native apps. Many native iOS apps cap Dynamic Type via UIFontMetrics maximumPointSize: for non-text-heavy UIs to avoid layout breakage.

62.2.2. Previewing in the simulator

The JavaSE simulator includes a Simulate → Larger Text menu that lets you preview every iOS Dynamic Type stop (Extra Small through Accessibility 5) without leaving your desktop. The simulator returns the same scale factor that the corresponding iOS device would report, so this is the recommended way to verify how your UI behaves at each setting before testing on hardware. The selected stop is persisted across simulator restarts.

If your app looks correct in the simulator at Large (default) but fonts appear oversized on your iPhone with the Display & Brightness slider near the middle, check whether Settings → Accessibility → Display & Text Size → Larger Text → Larger Accessibility Sizes is enabled on the device. When that toggle is on, the system reports the larger Accessibility size categories whose scales begin at 1.65× and exceed what the standard slider can reach.

62.3. Contacts API

The contacts API provides you with the means to query the phone’s address book, delete elements from it and create new entries into it. To get the platform specific list of contacts you can use String[] contacts = ContactsManager.getAllContacts();

Notice that on some platforms this will prompt the user for permissions and the user might choose not to grant that permission. To detect whether this is the case you can invoke isContactsPermissionGranted() after invoking getAllContacts(). This can help you adapt your error message to the user.

Once you have a Contact you can use the getContactById method, but the default method is a bit slow if you want to pull a large batch of contacts. The solution for this is to extract the data that you need through

Here you can specify true for the attributes that actually matter to you.

Another capability of the contacts API is the ability to extract all the contacts. This isn’t supported on all platforms but platforms such as Android can get a boost from this API as extracting the contacts one by one is slow on Android.

You can check if a platform supports the extraction of all the contacts through ContactsManager.isAllContactsFast().

When retrieving all the contacts, notice that you should probably not retrieve all the data and should set some fields to false to perform a more efficient query

You can then extract all the contacts using code that looks a bit like this, notice that you use a thread so the UI won’t be blocked!

List of contacts
Figure 257. List of contacts

Notice that you didn’t fetch the image of the contact as the performance of loading these images might be prohibitive. You can enhance the code above to include images by using slightly more complex code such as this:

The scheduleBackgroundTask method is like new Thread() in some regards. It places elements in a queue instead of opening too many threads so it can be good for non-urgent tasks
Contacts with the default photos on the simulator, on device these will use actual user photos when available
Figure 258. Contacts with the default photos on the simulator, on device these will use actual user photos when available
Notice that the code above uses callSerially & scheduleBackgroundTask in a liberal nested way. This is important to avoid an EDT violation

You can use createContact(String firstName, String familyName, String officePhone, String homePhone, String cellPhone, String email) to add a new contact and deleteContact(String id) to delete a contact.

62.3.1. Contact picker

The API above reads the whole address book, so it needs the broad contacts permission. If all you need is a contact the user chose, use ContactPicker instead. It shows the platform’s own picker, and the app only ever sees the contacts the user tapped and only the fields it asked for.

ContactPicker picker = new ContactPicker();
picker.setRequestedFields(ContactPicker.NAME | ContactPicker.PHONE);
picker.pick(ev -> {
    Contact[] picked = ContactPicker.getPickedContacts(ev);
    if(picked.length > 0) {
        numberField.setText(picked[0].getPrimaryPhoneNumber());
    }
});
From 2027-01-27 Google Play requires an app that targets Android 17 (API level 37) or later and requests READ_CONTACTS to pass a Play Console declaration explaining why the picker isn’t enough. An app that only picks never requests that permission, so it never files that declaration. The build only adds READ_CONTACTS for the broad API, so mixing the two in one app puts the permission back.

Ask for as little as you can. On Android the requested fields also decide which contacts the picker offers, and the platform returns nothing you didn’t ask for. setMultiSelect(boolean) lets the user pick several contacts, setSelectionLimit(int) caps how many, and setRequireAllRequestedFields(boolean) hides contacts that are missing one of them.

The contacts you get back are snapshots. There’s no continuing access behind them, so anything you need later has to be copied out of the Contact and stored by your app.

ContactPicker.isSupported() tells you whether the platform has a picker at all. Android, iOS and the simulator do; where a platform doesn’t, pick reports an empty selection rather than falling back to reading the address book, because that fallback would need the permission you were avoiding. A cancelled pick reports the same empty selection, so getPickedContacts is the only thing your listener has to check.

Android 17 and newer show the system picker, and serve every field. Older releases fall back to the contacts app’s own single-row picker, which needs no permission either but returns one contact carrying one kind of data: the name plus whichever of phone, email and postal address was requested first. A photo, a birthday or a web site is best-effort there, a multiple selection comes back as a single contact, and setRequireAllRequestedFields has no effect. Nothing about that escalates into a permission prompt - the fields you can’t have simply come back null, so read every one you asked for defensively.

62.4. Localization & internationalization (L10N & I18N)

Localization (l10n) means adapting to a locale which is more than translating to a specific language but also to a specific language within environment for example: en_US!= en_UK. Internationalization (i18n) is the process of creating one application that adapts to all locales and regional requirements.

Codename One supports automatic localization and seamless internationalization of an application using Java properties bundles.

Place locale-specific properties files inside an l10n directory directly under your module’s main directory (for example: common/src/main/l10n). Each file inside this directory is treated as a resource bundle and will be packaged automatically with your app, allowing you to version translations alongside the rest of your source code.

When you’re iterating on translations you can also use the simulator to capture bundles automatically. Open the Simulator menu and enable Auto Update Default Bundle so that running your app in the Codename One simulator will create any missing resource bundles on the fly as you interact with the UI, which makes it simple to populate keys without manually editing files during development. The resource is keyed by the properties file’s base name rather than by the directory holding it, so the archetype’s common/src/main/l10n/Bundle.properties becomes Bundle. The locale comes from the _xx suffix, and a file without one is filed under the empty locale, so ask for that when the device’s language has no bundle of its own:

// the bundle id is the .properties base name, not the directory: the
// archetype ships common/src/main/l10n/Bundle.properties
Hashtable<String, String> bundle = res.getL10N("Bundle", local);
if (bundle == null) {
    // a file with no _locale suffix is filed under the empty locale,
    // and getL10N is an exact lookup that will not fall back for you
    bundle = res.getL10N("Bundle", "");
}
UIManager.getInstance().setBundle(bundle);

The device language (as an ISO 639 two-letter code) could be retrieved with this:

String local = L10NManager.getInstance().getLanguage();

Once installed a resource bundle takes over the UI and every string set to a label (and label like components) will be automatically localized based on the bundle. You can also use the localize method of UIManager to perform localization on your own:

UIManager.getInstance().localize( "KeyInBundle", "DefaultValue");

The list of available languages in the resource bundle could be retrieved like this. Notice that this a list that was set by you and doesn’t need to confirm to the ISO language code standards:

Resources res = fetchResourceFile();
Enumeration locales = res.listL10NLocales("Bundle");

An exception for localization is the TextField/TextArea components both of which contain user data, in those cases the text won’t be localized to avoid accidental localization of user input.

You can export and import resource bundles as standard Java properties files, CSV and XML. The formats are pretty standard for most localization shops, the XML format Codename One supports is the one used by Android’s string bundles which means most localization specialists should localize it

The resource bundle is a map between keys and values for example: the code below displays "This Label is localized" on the Label with the hardcoded resource bundle. It would work the same with a resource bundle loaded from a resource file:

Form hi = new Form("L10N", new BoxLayout(BoxLayout.Y_AXIS));
HashMap<String, String> resourceBudle = new HashMap<String, String>();
resourceBudle.put("Localize", "This Label is localized");
UIManager.getInstance().setBundle(resourceBudle);
hi.add(new Label("Localize"));
hi.show();
Localized label
Figure 259. Localized label

62.4.1. Localization manager

The L10NManager class includes a multitude of features useful for common localization tasks.

It allows formatting numbers/dates & time based on platform locale. It also provides a great deal of the information you need such as the language/locale information you need to pick the proper resource bundle:

Form hi = new Form("L10N", new TableLayout(16, 2));
L10NManager l10n = L10NManager.getInstance();
// the parsers take locale-formatted input -- a hard coded "34.35" is
// read as 3435 anywhere "." groups digits -- so round-trip the
// formatters' own output
String localeDouble = l10n.format(34.35);
String localeCurrency = l10n.formatCurrency(33.77);
hi.add("format(double)").add(l10n.format(11.11)).
    add("format(int)").add(l10n.format(33)).
    add("formatCurrency").add(l10n.formatCurrency(53.267)).
    add("formatDateLongStyle").add(l10n.formatDateLongStyle(new Date())).
    add("formatDateShortStyle").add(l10n.formatDateShortStyle(new Date())).
    add("formatDateTime").add(l10n.formatDateTime(new Date())).
    add("formatDateTimeMedium").add(l10n.formatDateTimeMedium(new Date())).
    add("formatDateTimeShort").add(l10n.formatDateTimeShort(new Date())).
    add("getCurrencySymbol").add(l10n.getCurrencySymbol()).
    add("getLanguage").add(l10n.getLanguage()).
    add("getLocale").add(l10n.getLocale()).
    add("isRTLLocale").add("" + l10n.isRTLLocale()).
    add("parseCurrency").add(l10n.formatCurrency(l10n.parseCurrency(localeCurrency))).
    add("parseDouble").add(l10n.format(l10n.parseDouble(localeDouble))).
    add("parseInt").add(l10n.format(l10n.parseInt("56"))).
    add("parseLong").add("" + l10n.parseLong("4444444"));
hi.show();
Localization formatting/parsing and information
Figure 260. Localization formatting/parsing and information

62.4.2. RTL/Bidi

RTL stands for right to left, in the world of internationalization it refers to languages that are written from right to left (Arabic, Hebrew, Syriac, Thaana).

Most western languages are written from left to right (LTR), but some languages are written from right to left (RTL) speakers of these languages expect the UI to flow in the opposite direction otherwise it seems weird like reading this word would be to most English speakers: "drieW."

The problem posed by RTL languages is known as BiDi (Bi-directional) and not as RTL since the "true" problem isn’t the reversal of the writing/UI but rather the mixing of RTL and LTR together. For example: numbers are always written from left to right ( like in English) so in an RTL language the direction is from right to left and once you reach a number or English text embedded in the middle of the sentence (such as a name) the direction switches for a duration and is later restored.

The main issue in the Codename One world is in the layouts, which need to reverse on the fly. Codename One supports this through an RTL flag on all components that’s derived from the global RTL flag in UIManager.

Resource bundles can also include special case constant @rtl, which indicates if a language is written from right to left. This allows everything to automatically reverse.

When in RTL mode the UI will be the exact mirror so WEST will become EAST, RIGHT will become LEFT and this would be true for paddings/margins as well.

If you have a special case where you don’t want this behavior you will need to wrap it with an isRTL check. You can also use setRTL on a per Component basis to disable RTL behavior for a specific Component.

Most UI APIs have special cases for BiDi instead of applying it globally for example: AWT introduced constants such as LEADING instead of making WEST mean the opposite direction. You think that was a mistake since the cases where you wouldn’t want the behavior of automatic reversal are rare.

Codename One’s support for bidi includes the following components:

  • Bidi algorithm - allows converting between logical to visual representation for rendering

  • Global RTL flag - default flag for the entire application indicating the UI should flow from right to left

  • Individual RTL flag - flag indicating that the specific component/container should be presented as an RTL/LTR component (for example: for displaying English elements within an RTL UI).

  • RTL text field input

Most of Codename One’s RTL support is under the hood, the LookAndFeel global RTL flag can be enabled using:

UIManager.getInstance().getLookAndFeel().setRTL(true);

Once RTL is activated all positions in Codename One become reversed and the UI becomes a mirror of itself. For example: Adding a Toolbar command to the left will actually make it appear on the right. Padding on the left becomes padding on the right. The scroll moves to the left etc.

This applies to the layout managers (except for group layout) and most components. Bidi is seamless in Codename One but a developer still needs to be aware that his UI might be mirrored for these cases.

62.4.3. Localizing native iOS strings

Some strings in iOS need to be localized using iOS’s native mechanisms - namely providing *.lproj directories with .strings files. For example, if you want the app to have a different bundle display name for each language, or you want to translate the "UsageDescription" strings of your Info.plist into many languages, you would need to use iOS' native localization facilities.

Example: Localizing the app name

The app name, as it’s displayed to the user, is defined in using the CFBundleDisplayName key of the app’s Info.plist file., this will be automatically set to your app’s display name, as defined in your codenameone_settings.properties file. This works fine if your app will have the same name in every locale, but suppose you want your app to take on a different name in French than in English. For example: You want your app to be called "Hello App" for English-speaking users, and "Bonjour App" for French-speaking users.

In this case, you need to add iOS localization bundles "en.lproj" and fr.lproj, each with a file named InfoPlist.strings. If you’re using Maven, then you can add these directly inside the ios/src/main/strings directory of your project.

You will need to create the strings directory manually, if it doesn’t exist yet.
ios strings directory screenshot
Figure 261. Maven project with English, French, and Spanish localizations for Info.plist. English and French language bundles are contained in the ios/src/main/strings directory. The Spanish bundle is included as a zip file in ios/src/main/resources. Both methods are supported (zipped in resources and unzipped in strings).
Listing 21. ios/src/main/strings/en.lproj/InfoPlist.strings
"CFBundleDisplayName"="Hello App";
Listing 22. ios/src/main/strings/en.lproj/InfoPlist.strings
"CFBundleDisplayName"="Bonjour App";

The strings format is like the properties file format, except that both the "key" and the "value" must be wrapped in quotes. And if there are many strings, then they must be delimited by a semi-colon ;.

Legacy Ant Projects

Legacy Ant projects have a different directory structure. They have no equivalent location to the Maven ios/src/main/strings directory, but the legacy ios/src/main/resources content can be replicated under native/ios. To include native iOS localizations in those projects, place zipped versions of your .lproj directories inside the native/ios directory. For example, en.lproj.zip, fr.lproj.zip, etc.

Whenever possible, migrate legacy builds to Maven to take advantage of the modern workflow. See Chapter 2, Maven Project Workflow for automated migration options.
Example: Localization app usage description strings

iOS requires you to supply usage descriptions for many features that will be displayed to the user when the app requests permission to use the feature. For example, the NSCameraUsageDescription string must be provided if your app needs to use the camera. You can specify these values as build hints using the pattern ios.NSXXXUsageDescription=This feature is needed blah blah. In the NSCameraUsageDescription case, you might include the build hint:

ios.NSCameraUsageDescription=This app needs to use your camera to scan bar codes

These descriptions are embedded in your app’s Info.plist file, so they can be localized the same way you localize other Info.plist values - in the localized InfoPlist.strings file.

See the above example for instructions on localizing values in the Info.plist file. Then add translations to the InfoPlist.strings file for your usage descriptions.

Listing 23. ios/src/main/strings/en.lproj/InfoPlist.strings
"CFBundleDisplayName"="Hello App";
"NSCameraUsageDescription"="This app needs to use your camera to scan bar codes";
Listing 24. ios/src/main/strings/en.lproj/InfoPlist.strings
"CFBundleDisplayName"="Bonjour App";
"NSCameraUsageDescription"="Cette application doit utiliser votre appareil photo pour scanner les codes à barres";

62.4.4. Localizing the app icon

Codename One can ship a different launcher icon per locale on iOS and Android by detecting specially named PNG files during the build. Drop your per-locale artwork into common/src/main/resources using the convention:

cn1_icon_<lang>[_<country>].png

<lang> is a two-letter ISO 639-1 language code and the optional <country> is a two-letter ISO 3166-1 country code. Language is treated as case-insensitive; the country component is normalized to uppercase. A few valid examples:

  • cn1_icon_fr.png: French (any region)

  • cn1_icon_en_GB.png: British English

  • cn1_icon_es_MX.png: Mexican Spanish

Supply a square source image at least 432×432 pixels (the largest size emitted for Android adaptive icons; 1024×1024 is recommended, matching the main app icon); the build resizes it to every target density. A smaller source would be upscaled and look blurry, so the build now fails with a clear error when it detects an undersized localized icon rather than shipping a soft one. The default app icon continues to be controlled by your codenameone_settings.properties file and is used whenever the device locale doesn’t match any of the localized variants.

These icons live under src/main/resources and Maven copies them into target/classes incrementally — it doesn’t delete the copy when you remove or rename the source file. If you replace a localized icon and still see the old (often blurry) one, run mvn clean to clear the stale resource from target/classes before rebuilding. The build also fails when it finds an undersized cn1_icon_*.png in the compiled output that’s about to be bundled and sent to the build server.

At runtime the builders look for a <lang>_<COUNTRY> match first, then fall back to a bare <lang> match. Providing both (for example cn1_icon_en.png plus cn1_icon_en_GB.png) lets you give British users a country-specific icon while every other English locale still receives the generic English icon.

Android behavior

On Android the build generates locale-qualified drawable resources at every density so the platform picks the right icon automatically based on the device’s current locale:

  • drawable-<lang>[-r<COUNTRY>][-<dpi>]/icon.png at 36, 48, 72, 96, 128, 144 and 192 pixels for legacy launchers.

  • When android.enableAdaptiveIcons=true the same variants are also written as mipmap-<lang>[-r<COUNTRY>]-<dpi>/ic_launcher.png and ic_launcher_foreground.png. The default adaptive background and the mipmap-anydpi-v26/ic_launcher.xml definition are reused as-is.

No code changes are required—Android’s resource framework switches icons when the locale changes.

When you supply a region-qualified icon (such as cn1_icon_ar_AE.png) without a matching language-only variant, the build also emits the default (non-localized) icon into drawable-<lang>/ and the matching mipmap-*-<lang>/ directories. This barrier is required because Android’s resource resolver (API 24+) walks every child of the parent locale when it can’t find an exact or parent-language match, and would otherwise pick ar-rAE for, say, an ar-PK device. The barrier short-circuits that lookup so only devices whose region matches the supplied variant receive the localized icon. If you also ship a language-only file (for example cn1_icon_ar.png) it’s used as the barrier instead, so you keep full control of the fallback icon for Arabic speakers outside AE.

iOS behavior

iOS doesn’t localize launcher icons natively, so Codename One wires up alternate app icons for you:

  • For each detected locale the build generates an AppIcon_<lang>_<COUNTRY>.appiconset inside Images.xcassets containing the 120×120, 180×180, 152×152 and 167×167 (iPad Pro) PNGs along with a matching Contents.json.

  • The build adds the matching ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES Xcode build setting so actool emits a coherent partial Info.plist covering both CFBundlePrimaryIcon and CFBundleAlternateIcons. The user’s Info.plist is left untouched; injecting CFBundleIcons manually would be dropped during the actool merge and is therefore avoided.

  • The CodenameOne_GLAppDelegate is patched to call -[UIApplication setAlternateIconName:completionHandler:] at launch. The delegate reads [NSLocale preferredLanguages], tries the full <lang>_<COUNTRY> key first, then falls back to the language-only key, and clears the alternate icon (reverting to the default) if no variant matches.

  • The injection is idempotent and runs before the ios.afterFinishLaunching hook, so any custom code you supply via that build hint is unaffected.

IOS displays a system alert the first time an app switches to an alternate icon. This is platform-standard behavior—Codename One can’t suppress it.
Tips and troubleshooting
  • File names are matched case-insensitively. cn1_icon_EN_uk.png is treated the same as cn1_icon_en_UK.png.

  • Names that don’t fit the <2-letter lang>[_<2-letter country>] pattern are logged and skipped; the rest of the build continues.

  • The original cn1_icon_*.png files are removed after processing so they aren’t shipped as stray bundle resources.

  • To override the default (non-localized) icon, keep using the Icon setting in codenameone_settings.properties or the icon image in CN1Resource.res. This feature only adds alternate icons for specific locales.

62.5. Location - GPS

The Location API allows you to track changes in device location or the current user position.

The Simulator includes a Location Simulation tool that you can launch to determine the current position of the simulator and debug location events

The most basic usage for the API allows you to fetch a device Location, notice that this API is blocking and can take a while to return:

Location position = LocationManager.getLocationManager().getCurrentLocationSync();
In order for location to work on iOS you MUST define the build hint ios.locationUsageDescription and describe why your application needs access to location. Otherwise you won’t get location updates!

The getCurrentLocationSync() method is good for cases where you need to fetch a current location once and not repeatedly query location. It activates the GPS then turns it off to avoid excessive battery usage. For example, if an application needs to track motion or position over time it should use the location listener API to track location as such:

Notice that there is a method called getCurrentLocation() which will return the current state and might not be right for some cases:
class MyListener implements LocationListener {
    public void locationUpdated(Location location) {
        // update UI etc.
    }

    public void providerStateChanged(int newState) {
        // handle status changes/errors appropriately
    }
}
LocationManager.getLocationManager().setLocationListener(new MyListener());
On Android location maps to low-level APIs if you disable the usage of Google Play Services. By default location should perform well if you leave the Google Play Services on

62.5.1. Location in the background - geofencing

Polling location is expensive and requires a special permission on iOS. Its also implemented rather differently both in iOS and Android. Both platforms place restrictions on the location API usage in the background.

Because of the nature of background location the API is non-trivial. It starts with the venerable LocationManager but instead of using the standard API you need to use setBackgroundLocationListener.

Instead of passing a LocationListener instance you need to pass a Class object instance. This is important because background location might be invoked when the app isn’t running and an object would need to be allocated.

Notice that you shouldn’t perform long operations in the background listener callback. IOS wake-up time is limited to about 10 seconds and the app could get killed if it exceeds that time slice.

Notice that the listener can also send events when the app is in the foreground, so it’s recommended to check the app state before deciding how to process this event. You can use Display.isMinimized() to determine if the app is running or in the background.

When implementing this make sure that:

  • The class passed to the API is a public class in the global scope. Not an inner class or anything like that!

  • The class has a public no-argument constructor

  • You need to pass it as a class literal for example: MyClassName.class. Don’t use Class.forName("my.package.MyClassName")!
    Class names are problematic since device builds are obfuscated, you should use literals which the obfuscator detects and handles.

The following code demonstrates usage of the Geofence API:

Geofence gf = new Geofence("test", loc, 100, 100000);

LocationManager.getLocationManager()
        .addGeoFencing(GeofenceListenerImpl.class, gf);

The listener it registers looks like this:

public class GeofenceListenerImpl implements GeofenceListener {
    @Override
    public void onExit(String id) {
    }

    @Override
    public void onEntered(String id) {
        if(Display.getInstance().isMinimized()) {
            // backgrounded, so there is no form to show a dialog on
            LocalNotification ln = new LocalNotification();
            ln.setId("geofence-welcome");
            ln.setAlertTitle("Welcome");
            ln.setAlertBody("Thanks for arriving!");
            Display.getInstance().scheduleLocalNotification(ln,
                    System.currentTimeMillis() + 10, LocalNotification.REPEAT_NONE);
        } else {
            Display.getInstance().callSerially(() -> {
                Dialog.show("Welcome", "Thanks for arriving", "OK", null);
            });
        }
    }
}
Android background location permissions (API 30+)

On Android 11 (API level 30) and higher, requesting background location permission requires a two-step process. First, foreground location permissions must be granted. Then, the app must request background location access, which will direct the user to the system settings to select Allow all the time. Codename One handles this flow automatically when you use LocationManager.

For Android 11+ (API 30+), Codename One detects if background location is needed and presents a dialog explaining the need before redirecting the user to the app settings. You can customize both dialogs through localization keys under android.permission.ACCESS_BACKGROUND_LOCATION. The first one, offering to open the settings screen, takes its body from the bare key and its title and buttons from .title, .settings and .cancel. The second, shown after the user comes back, uses .explanation_title, .explanation_body and .ok:

// the port reads these through UIManager.localize(). There is only one
// bundle, so add to the installed one rather than replacing it -- a
// fresh map here would drop every other localized string in the app.
// setBundle keeps whatever map it was handed, so the installed one may
// be immutable -- copy before adding
java.util.Map<String, String> installed = UIManager.getInstance().getBundle();
java.util.Map<String, String> bundle = installed == null
        ? new java.util.HashMap<String, String>()
        : new java.util.HashMap<String, String>(installed);
// the bare key is the body of the first prompt; the suffixed ones
// dress the follow-up dialog that sends the user to settings
bundle.put("android.permission.ACCESS_BACKGROUND_LOCATION",
        "Reminders need your location while the app is closed.");
bundle.put("android.permission.ACCESS_BACKGROUND_LOCATION.title",
        "Location needed");
bundle.put("android.permission.ACCESS_BACKGROUND_LOCATION.explanation_body",
        "Choose \"Allow all the time\" so reminders still arrive when the app is closed.");
bundle.put("android.permission.ACCESS_BACKGROUND_LOCATION.explanation_title",
        "One more step");
bundle.put("android.permission.ACCESS_BACKGROUND_LOCATION.settings", "Open settings");
bundle.put("android.permission.ACCESS_BACKGROUND_LOCATION.ok", "Done");
bundle.put("android.permission.ACCESS_BACKGROUND_LOCATION.cancel", "Not now");
UIManager.getInstance().setBundle(bundle);

62.5.2. One-time location - the location button

Google Play draws a line between two ways an application uses precise location. Tracking, navigation and geofencing are ongoing, and an application that does them holds ACCESS_FINE_LOCATION and declares why. A transactional use is different: finding restaurants nearby, filling in an address, tagging a photo. For those, applications that target Android 17 (API level 37) must let the user share their location through a button that the system draws, and the deadline is January 27, 2027.

LocationButton is that control:

LocationButton locationButton =
        new LocationButton(LocationButton.TEXT_USE_PRECISE_LOCATION);
locationButton.addLocationSharedListener(loc -> {
    if (loc == null) {
        status.setText("Location not shared");
    } else {
        status.setText(loc.getLatitude() + ", " + loc.getLongitude());
    }
    status.getParent().revalidate();
});
root.add(locationButton);

The listener is called once the user has answered, with a Location or with null. null means the user declined, or that no fix arrived before the button’s timeout. A platform control that fails to open isn’t reported this way: the component replaces it with the ordinary button, so the user is asked again there rather than told no. isSystemRendered() is how to see that happened.

Where the system draws it, and where it doesn’t

On Android 17 and later this component is the system’s control. Another process renders it, and the application can neither change its wording nor intercept its taps. That’s the point: a tap on a button the system drew is what earns the session-scoped grant, so the application never has to hold precise location permanently.

Everywhere else — on Android below API level 37, on iOS, on the desktop and in the simulator — the component is an ordinary Codename One button that asks for the location permission the usual way, styled through the LocationButton UIID like any other component. Your code is the same either way. isSystemRendered() on the button tells you which of the two it ended up with, for an application that wants to word something differently around it. It answers for that button rather than for the platform, so it’s also false on an Android 17 device where the system control failed to open and the component fell back. To ask the platform question before you build anything, use Display.getInstance().isLocationButtonSupported().

The label comes from the text type you pass to the constructor, one of TEXT_NONE, TEXT_PRECISE_LOCATION, TEXT_USE_PRECISE_LOCATION, TEXT_SHARE_PRECISE_LOCATION, TEXT_NEAR_MY_PRECISE_LOCATION or TEXT_NEAR_YOUR_PRECISE_LOCATION. setButtonBackgroundColor() and setButtonTextColor() reach the system control; the fallback button follows the theme.

Permissions

Nothing to configure. Referencing LocationButton makes the Android build add USE_LOCATION_BUTTON to the manifest, which the platform requires before it will render the control.

The build also decides how to declare ACCESS_FINE_LOCATION. An application that uses the button and nothing else from the location or maps packages gets it declared onlyForLocationButton, which means the system grants precise location through the button and never any other way: no "allow precise location" question, nothing held between taps, and nothing to justify to Google Play. An application that also tracks, navigates or geofences needs the ordinary grant and gets the ordinary declaration. The class scan the build already runs answers this, so neither case needs a build hint.

The one thing it can’t see is native Android code reaching the platform’s location APIs directly, because Gradle compiles that after the decision is made. An application like that declares itself:

codename1.arg.android.locationButton.exclusive=false

true forces the restriction on and false forces it off; the default, auto, infers it. Force it off when native code needs the ordinary grant — the failure is otherwise silent, with location requests coming back approximate rather than refusing.

One consequence is worth knowing. The restriction is what it says: precise location comes through the button and no other way. On an application that carries it, the fallback described above can’t recover precise location — if the system control fails to open, the ordinary button that replaces it asks a question the system won’t answer with precise access, and the listener receives an approximate location or none. That’s the bargain the restriction makes, and it’s the right one for an application whose only use of precise location is the button; it’s another reason not to force it on for an application that reaches precise location some other way.

62.6. Background music playback

Codename One supports playing music in the background (for example: when the app is minimized) which is useful for developers building a music player style application.

This support isn’t totally portable since the Android and iOS approaches for background music playback differ a great deal. To get this to work on Android you need to use the API: MediaManager.createBackgroundMedia().

You should use that API when you want to create a media stream that will work even when your app is minimized.

For iOS you will need to use a special build hint: ios.background_modes=music.

Which should allow background playback of music on iOS and would work with the createBackgroundMedia() method.

62.7. Capture - photos, video, audio

The capture API allows you to use the camera to capture photographs or the microphone to capture audio. It even includes an API for video capture.
The API itself couldn’t be simpler:

String filePath = Capture.capturePhoto();

Just captures and returns a path to a photo you can either open it using the Image class or save it somewhere.

The returned file is a temporary file, you shouldn’t store a reference to it and instead copy it locally or work with the Image object

For example: you can copy the Image to Storage using:

String filePath = Capture.capturePhoto();
if(filePath != null) {
    Util.copy(FileSystemStorage.getInstance().openInputStream(filePath), Storage.getInstance().createOutputStream(myImageFileName));
}
When running on the simulator the Capture API opens a file chooser API instead of physically capturing the data. This makes debugging device or situation specific issues simpler

You can capture an image from the camera using an API like this:

Form hi = new Form("Capture", new BorderLayout());
hi.setToolbar(new Toolbar());
Style s = UIManager.getInstance().getComponentStyle("Title");
FontImage icon = FontImage.createMaterial(FontImage.MATERIAL_CAMERA, s);

ImageViewer iv = new ImageViewer(icon);

hi.getToolbar().addCommandToRightBar("", icon, (ev) -> {
    String filePath = Capture.capturePhoto();
    if(filePath != null) {
        try {
            DefaultListModel<Image> m = (DefaultListModel<Image>)iv.getImageList();
            Image img = Image.createImage(filePath);
            if(m == null) {
                m = new DefaultListModel<>(img);
                iv.setImageList(m);
                iv.setImage(img);
            } else {
                m.addItem(img);
            }
            m.setSelectedIndex(m.getSize() - 1);
        } catch(IOException err) {
            Log.e(err);
        }
    }
});

hi.add(BorderLayout.CENTER, iv);
hi.show();
Captured photos previewed in the ImageViewer
Figure 262. Captured photos previewed in the ImageViewer

You show video capture in the MediaManager section.

The sample below captures audio recordings (using the 'Capture' API) and copies them locally under unique names. It also demonstrates the storage and organization of captured audio:

Form hi = new Form("Capture", BoxLayout.y());
hi.setToolbar(new Toolbar());
Style s = UIManager.getInstance().getComponentStyle("Title");
FontImage icon = FontImage.createMaterial(FontImage.MATERIAL_MIC, s);

FileSystemStorage fs = FileSystemStorage.getInstance();
String recordingsDir = fs.getAppHomePath() + "recordings/";
fs.mkdir(recordingsDir);
try {
    for(String file : fs.listFiles(recordingsDir)) {
        MultiButton mb = new MultiButton(file.substring(file.lastIndexOf("/") + 1));
        mb.addActionListener((e) -> {
            try {
                Media m = MediaManager.createMedia(recordingsDir + file, false);
                m.play();
            } catch(IOException err) {
                Log.e(err);
            }
        });
        hi.add(mb);
    }

    hi.getToolbar().addCommandToRightBar("", icon, (ev) -> {
        try {
            String file = Capture.captureAudio();
            if(file != null) {
                SimpleDateFormat sd = new SimpleDateFormat("yyyy-MMM-dd-kk-mm");
                String fileName =sd.format(new Date());
                String filePath = recordingsDir + fileName;
                Util.copy(fs.openInputStream(file), fs.openOutputStream(filePath));
                MultiButton mb = new MultiButton(fileName);
                mb.addActionListener((e) -> {
                    try {
                        Media m = MediaManager.createMedia(filePath, false);
                        m.play();
                    } catch(IOException err) {
                        Log.e(err);
                    }
                });
                hi.add(mb);
                hi.revalidate();
            }
        } catch(IOException err) {
            Log.e(err);
        }
    });
} catch(IOException err) {
    Log.e(err);
}
hi.show();
Captured recordings in the demo
Figure 263. Captured recordings in the demo

You can also record audio through MediaRecorderBuilder, which describes a recording by path, MIME type, sample rate, bit rate and channel count instead of handing the job to the platform’s own capture UI.

Build it with MediaRecorderBuilder.build(), which passes the whole builder to the platform implementation.

MediaManager.createMediaRecorder(MediaRecorderBuilder) accepts the same object, and what it does with it depends on the destination. A builder with redirectToAudioBuffer(true) is delegated straight to build(), so every setting survives. A builder recording to a file is reduced to its path and MIME type, which drops the sample rate, bit rate and channel count set on it and leaves the platform’s defaults in place. A stereo request isn’t refused so much as ignored, and what you get is whatever that platform would have recorded anyway.

Call build() yourself when those settings matter, bearing in mind how far each port carries them. Android, iOS, the JavaScript port and the simulator implement the builder overload in full. The native Linux and Windows ports implement it too but read only the sampling rate and channel count, their WAV recorders fixing the MIME type and ignoring the bit rate; they also take no notice of redirectToAudioBuffer(true), so asking those two for live PCM writes a file at the builder’s path instead. Anywhere else the framework’s own fallback reduces the builder to a path and a MIME type again.

How far the settings then travel depends on the port, and on the format. The JavaScript port takes only the path when recording to a file, so a web file recording uses the browser’s defaults; recording into an audio buffer it does ask for the sampling rate — though not always. It skips the request on iOS whatever the browser, and on any browser that doesn’t advertise the sampleRate constraint, so those recordings arrive at the platform’s own rate. Elsewhere the whole builder reaches the port, which applies what its codec supports: Android sets the sampling rate and bit rate when encoding AAC and leaves both to the encoder for AMR, and the simulator never reads the bit rate at all.

Treat the builder’s numbers as preferences rather than guarantees. An AudioBuffer reports getSampleRate() for the data it holds — the recorder fills that in from the rate the audio actually arrived at, or from the rate it resampled the audio to, so the figure describes the samples in your hands.

getNumChannels() reports the layout the recorder built, and on the JavaScript port that much is exact: it emits channel 0 alone for a mono request and interleaves left and right for a stereo one, so the count always matches the PCM you hold and you can split the channels by it. It says nothing about the capture, though — the port never submits a channel count to the browser, so a stereo request depends on the input device already providing two channels.

62.7.1. Capture asynchronous API

The Capture API also includes a callback based API that uses the ActionListener interface to implement capture. For example: you can adapt the previous sample to use this API as such:

hi.getToolbar().addCommandToRightBar("", icon, (ev) -> {
    Capture.capturePhoto((e) -> {
        if(e != null && e.getSource() != null) {
            try {
                DefaultListModel<Image> m = (DefaultListModel<Image>)iv.getImageList();
                Image img = Image.createImage((String)e.getSource());
                if(m == null) {
                    m = new DefaultListModel<>(img);
                    iv.setImageList(m);
                    iv.setImage(img);
                } else {
                    m.addItem(img);
                }
                m.setSelectedIndex(m.getSize() - 1);
            } catch(IOException err) {
                Log.e(err);
            }
        }
    });
});

The gallery API allows picking an image and/or video from the cameras gallery (camera roll).

Like the Capture API the image returned is a temporary image that should be copied locally, this is due to device restrictions that don’t allow direct modifications of the gallery

You can adapt the Capture sample above to use the gallery as such:

Form hi = new Form("Capture", new BorderLayout());
hi.setToolbar(new Toolbar());
Style s = UIManager.getInstance().getComponentStyle("Title");
FontImage icon = FontImage.createMaterial(FontImage.MATERIAL_CAMERA, s);

ImageViewer iv = new ImageViewer(icon);

hi.getToolbar().addCommandToRightBar("", icon, (ev) -> {
    Display.getInstance().openGallery((e) -> {
        if(e != null && e.getSource() != null) {
            try {
                DefaultListModel<Image> m = (DefaultListModel<Image>)iv.getImageList();
                Image img = Image.createImage((String)e.getSource());
                if(m == null) {
                    m = new DefaultListModel<>(img);
                    iv.setImageList(m);
                    iv.setImage(img);
                } else {
                    m.addItem(img);
                }
                m.setSelectedIndex(m.getSize() - 1);
            } catch(IOException err) {
                Log.e(err);
            }
        }
    }, Display.GALLERY_IMAGE);
});

hi.add(BorderLayout.CENTER, iv);
No need for a screenshot as it will look identical to the capture image screenshot above

The last value is the type of content picked which can be one of: Display.GALLERY_ALL, Display.GALLERY_VIDEO or Display.GALLERY_IMAGE.

62.9. Analytics integration

Analytics now has its own chapter. It covers the provider SPI, consent and privacy handling, the built-in providers (the Codename One first-party service, Google Analytics 4, Matomo, Firebase and a logging provider), and how to write your own. See the Analytics chapter.

The AnalyticsService class documented here previously is deprecated but still works — it delegates to the new API. The migration table at the end of the Analytics chapter maps each old call to its replacement.

62.10. Social sign-in (Facebook, Google, …​)

The legacy Facebook and Google OAuth2 sign-in flows that used to live here drove an embedded WebBrowser, which the providers no longer permit. The full sign-in stack — system-browser-based OpenID Connect for any provider, plus Facebook / Google / Microsoft / Apple / Auth0 / Firebase helpers — now lives in the Authentication and Identity chapter.

62.11. Lead Component

Codename One has two basic ways to create new components:

  1. Subclass a Component override paint, implement event callbacks etc.

  2. Compose many components into a new component, by subclassing a Container.

Components such as Tabs subclass Container which make a lot of sense for that component since it’s physically a Container.

For example, components like MultiButton, SpanButton & SpanLabel don’t necessarily seem like the right candidate for compositing but they’re all Container subclasses.

Using a Container provides you a lot of flexibility in layout & functionality for a specific component. MultiButton is a great example of that. It’s a Container internally that’s composed of 5 labels and a Button.

Codename One makes the MultiButton "feel" like a single button through the use of setLeadComponent(Component) which turns the button into the "leader" of the component.

When a Container hierarchy is placed under a leader all events within the hierarchy are sent to the leader, so if a label within the lead component receives a pointer pressed event this event will be sent to the leader.

For example: for the MultiButton the internal button will receive that event and send the action performed event, change the state etc.

This creates some potential issues for instance in MultiButton:

myMultiButton.addActionListener((e) -> {
    if(e.getComponent() == myMultiButton) {
        // this won't occur since the source component is really a button!
    }
    if(e.getActualComponent() == myMultiButton) {
        // this will happennull
    }
});

The leader also determines the style state, so all the elements being lead are in the same state. For example: if the button is pressed all elements will display their pressed states, notice that they will do so with their own styles but they will each pick the pressed version of that style so a Label UIID within a lead component in the pressed state would return the Pressed state for a Label not for the Button.

This is convenient when you need to construct more elaborate UI’s and the cool thing about it’s that you can do this entirely in the designer which allows assembling containers and defining the lead component inside the hierarchy.

For example: the SpanButton class is like this code:

public class SpanButton extends Container {
    private Button actualButton;
    private TextArea text;

    public SpanButton(String txt) {
        setUIID("Button");
        setLayout(new BorderLayout());
        text = new TextArea(getUIManager().localize(txt, txt));
        text.setUIID("Button");
        text.setEditable(false);
        text.setFocusable(false);
        actualButton = new Button();
        addComponent(BorderLayout.WEST, actualButton);
        addComponent(BorderLayout.CENTER, text);
        setLeadComponent(actualButton);
    }


    public void setText(String t) {
        text.setText(getUIManager().localize(t, t));
    }

    public void setIcon(Image i) {
        actualButton.setIcon(i);
    }

    public String getText() {
        return text.getText();
    }

    public Image getIcon() {
        return actualButton.getIcon();
    }

    public void addActionListener(ActionListener l) {
        actualButton.addActionListener(l);
    }

    public void removeActionListener(ActionListener l) {
        actualButton.removeActionListener(l);
    }

}

62.11.1. Blocking lead behavior

The Component class has two methods that allow you to exclude a component from lead behavior: setBlockLead(boolean) & isBlockLead().

Effectively when you have a Component within the lead hierarchy that you would like to treat differently from the rest you can use this method to exclude it from the lead component behavior while keeping the rest in line…​

This should have no effect if the component isn’t a part of a lead component.

The sample below is based on the Accordion component which uses a lead component internally:

f.getToolbar().addMaterialCommandToRightBar("", FontImage.MATERIAL_ADD, e -> addEntry(accr));
addEntry(accr);
f.add(BorderLayout.CENTER, accr);
f.show();

This allows you to add/edit entries but it also allows the delete button above to actually work. Without a call to setBlockLead(true) the delete button would cat as the rest of the accordion title.

Accordion with delete button entries that work despite the surrounding lead
Figure 264. Accordion with delete button entries that work despite the surrounding lead

62.12. Pull to refresh

Pull to refresh is the common UI paradigm that Twitter popularized where the user can pull down the form/container to receive an update. Adding this to Codename One couldn’t be simpler!

Just invoke addPullToRefresh(Runnable) on a scrollable container (or form) and the runnable method will be invoked when the refresh operation occurs.

Pull to refresh is implicitly implements in the InfiniteContainer
Form hi = new Form("Pull To Refresh", BoxLayout.y());
hi.getContentPane().addPullToRefresh(() -> {
    hi.add("Pulled at " + L10NManager.getInstance().formatDateTimeShort(new Date()));
});
hi.show();

setPullToRefresh(Runnable) is provided as a more discoverable alias for the same single-task slot — both names route to the same field, and a second call replaces whatever runnable was previously registered.

Simple pull to refresh demo
Figure 265. Pull to refresh demo

62.12.1. Modern arc-spinner pull-to-refresh

The legacy pull-to-refresh visual is the classic "rotating arrow + text" stack. Modern Material 3 / iOS apps instead show a thin circular arc that grows as the user pulls and spins once the task fires. This is shipped as an opt-in path via the pullToRefreshModernBool theme constant; the iOS Modern and Android Material themes enable it by default so apps shipping against those themes get the spec-accurate look without any extra wiring.

To enable in a custom theme:

#Constants {
    pullToRefreshModernBool: true;
    pullToRefreshIndicatorDiameterMm: 8;       /* 8mm circle           */
    pullToRefreshIndicatorStrokeMm: "0.6";     /* 0.6mm stroke thickness */
}

TabIndicator {
    /* The pull-to-refresh arc shares its color with the animated tab
       indicator -- both follow the brand's accent. */
    color: #007aff;
    background-color: transparent;
    padding: 0;
    margin: 0;
}

Behavior:

  • During the pull gesture the arc sweep grows from 0° to ~330° proportional to how far the user has pulled past the threshold.

  • When the refresh task fires the arc rotates continuously at ~360°/sec until the task completes.

  • All sizing is in millimetres so the indicator stays physical-size-accurate across device densities; no rasterized images involved.

The legacy addPullToRefresh API is unchanged — the same Runnable is invoked at the same moment; only the visual rendering differs.

62.13. Running 3rd party apps using display’s execute

The Display class’s execute method allows you to invoke a URL which is bound to a particular application.

This works rather well assuming the application is installed. Apple’s URL scheme reference lists the schemes iOS handles itself, for common applications and built-in functionality.

Some URLs might not be supported if an app isn’t installed, on Android there isn’t much that can be done but iOS has a canOpenURL method for Objective-C.

On iOS you can use the Display.canExecute() method which returns a Boolean instead of a boolean which allows you to support 3 result states:

  1. Boolean.TRUE - the URL can be executed

  2. Boolean.FALSE - the URL isn’t supported or the app is missing

  3. null - you’ve no idea whether the URL will work on this platform.

The sample below launches a "godfather" search on IMDB when this is sure to work ( on iOS currently). You can actually try to search for null as well but this sample plays it safe by using the http link which is sure to work:

Boolean can = Display.getInstance().canExecute("imdb:///find?q=godfather");
if(can != null && can) {
    Display.getInstance().execute("imdb:///find?q=godfather");
} else {
    Display.getInstance().execute("http://www.imdb.com");
}

62.14. Automatic build hint configuration

You try to make Codename One "seamless," this expresses itself in small details such as the automatic detection of permissions on Android etc. The build servers go a long way in setting up the environment as intuitive. However, it isn’t enough, build hints are often confusing and obscure. It’s hard to abstract the mess that’s native mobile OSes and the odd policies from Apple/Google…​

A good example for a common problem developers face is location code that doesn’t work in iOS. This is due to the ios.locationUsageDescription build hint that’s required. The reason that build hint was added is a need by Apple to provide a description for every app that uses the location service.

To solve this sort of used case you have two APIs in Display:

void ensureLocationUsageDescription() {
    // Both calls only work in the simulator, and setProjectBuildHint throws
    // anywhere else -- so this belongs behind an isSimulator() check, run
    // once during development rather than on every launch.
    if (!Display.getInstance().isSimulator()) {
        return;
    }

    Map<String, String> hints = Display.getInstance().getProjectBuildHints();
    if (hints == null) {
        // No codename1_settings.properties beside the running project, or
        // it could not be read. Nothing to check against, and nothing to
        // write into.
        return;
    }

    String description = hints.get("ios.locationUsageDescription");
    if (description == null || description.length() == 0) {
        Display.getInstance().setProjectBuildHint("ios.locationUsageDescription",
                "Used to show nearby results on the map");
    }
}

Both of these allow you to detect if a build hint is set and if not (or if it’s set incorrectly) set its value…​

If you use the location API from the simulator and you didn’t define ios.locationUsageDescription Codename One will implicitly define a string there. The cool thing is that you will now see that string in your settings and you would be able to customize it.

For example, this gets way better than that trivial example!

The real value is for 3rd party cn1lib authors. For example: Google Maps or Parse. They can inspect the build hints in the simulator and show an error in case of a misconfiguration. They can even show a setup UI. Demos that need special keys in place can force the developer to set them up before continuing.

62.15. Easy thread

Working with threads is ranked as one of the least intuitive and painful tasks in programming. This is such an error prone task that some platforms/languages took the route of avoiding threads entirely. A typical scenario: you need to convert some code to work on a separate thread but you still want the ability to communicate and transfer data from that thread.

This is possible in Java but non-trivial, the thing is that this is easy to do in Codename One with tools such as callSerially that let arbitrary code run on the EDT. Why not offer that to any random thread?

That’s why EasyThread exists: it takes some concepts of Codename One’s threading and makes them more accessible to an arbitrary thread. This way you can move things like resource loading into a separate thread and synchronize the data back into the EDT as needed…​

Easy thread can be created like this:

EasyThread e = EasyThread.start("ThreadName");

You can send a task to the thread using:

e.run(() -> doThisOnTheThread());

However, it gets better, say you want to return a value:

// the result callback runs on the EasyThread, not the EDT, so anything
// that touches the UI has to be marshalled back explicitly
e.run((success) -> success.onSucess(doThisOnTheThread()),
      (myResult) -> Display.getInstance().callSerially(() -> onEDTGotResult(myResult)));

Lets break that down. (success) → success.onSucess(doThisOnTheThread()) runs on the EasyThread, off the EDT, and the result callback runs there too: EasyThread hands your SuccessCallback straight to the worker and never marshals it. Anything that touches the UI has to be sent back with callSerially, which is what the second lambda does.

These asynchronous calls make things a bit painful to wade through, so the API wraps them in a simplified synchronous version:

EasyThread e = EasyThread.start("Hi");
int result = e.run(() -> {
    System.out.println("This is a thread");
    return 3;
});
// the worker stays alive waiting for more work, so release it when the
// thread has nothing left to do
e.killWhenIdle();

There are a few other variants like runAndWait and there is a kill() method which stops a thread and releases its resources.

62.16. Mouse cursor

Codename one can change the mouse cursor when hovering over specific areas to show resizability, movability etc. For obvious reasons this feature is available in the desktop and JavaScript ports as the other ports rely on touch interaction. The feature is off by default and needs to be enabled on a Form by using Form.setEnableCursors(true);. If you’re writing a custom component that can use cursors such as SplitPane you can use:

@Override
protected void initComponent() {
    super.initComponent();
    getComponentForm().setEnableCursors(true);
}

Once this is enabled you can set the cursor over a specific region using cmp.setCursor() which accepts one of the cursor constants defined in Component.

62.17. Working with git

A generated Maven project ships a .gitignore for its build output — target, build, and the RAD schema generated under common/src/main/rad. It says nothing about your editor, and that’s deliberate, because most of what sits in those directories belongs to the project rather than to you. The archetype generates .idea and .vscode itself, including run configurations for the debug proxy, on-device debugging and the certificate wizard, and a CodeRAD project needs common/.settings/org.eclipse.m2e.apt.prefs before Eclipse will run the annotation processor. Commit those.

The part worth ignoring is the narrow set that records what you personally had open, chiefly .idea/workspace.xml, .idea/shelf/ and *.iml. That set is the same in every project you work on, which makes it a good fit for a global ignore file rather than a per-project one — git config --global core.excludesFile ~/.gitignore_global, with the patterns in the file it names.

The theme used to be the hard part here, because a .res is a binary that git can’t diff, and two people editing one produce a conflict no merge tool can resolve. A Maven project doesn’t have that problem. The theme’s source is common/src/main/css/theme.css, and the .res is compiled out of it into target/classes on each build, so what’s under version control is text and the binary is an artifact the .gitignore above already excludes.

That leaves the resource editor’s File → XML Team Mode, which writes a matching res/theme.xml tree beside a resource file so its contents diff and merge like source. It’s on by default, but it only fires for the older Ant layout, because saveXML wants the resource’s own directory to be named src with codenameone_settings.properties beside it, and no Maven source path looks like that. Expect to meet it in an inherited project rather than to adopt it in a new one.

AI and agents

63. AI, Chat UI, and Speech

Codename One ships a portable LLM client, a streaming chat component, speech-to-text and text-to-speech APIs, built-in vision and language services, and a portable LiteRT inference API. The builders add native frameworks and dependencies when application bytecode references the corresponding entry point.

This chapter introduces every piece in turn:

  • com.codename1.ai — provider-agnostic chat, embeddings, tool calls, and image generation.

  • com.codename1.components.ChatView — a theme-aware chat surface that streams tokens in place.

  • com.codename1.media.SpeechRecognizer and com.codename1.media.TextToSpeech — on-device speech-to-text and speech synthesis.

  • com.codename1.security.SecureStorage non-prompting overloads — silent reads for secrets the network layer needs on every call, such as LLM API keys.

  • com.codename1.ai.vision — OCR, barcode, face, labeling, pose, segmentation, document correction, and camera-frame pipelines.

  • com.codename1.ai.inference — reusable LiteRT sessions for .tflite models.

  • com.codename1.ai.language — language identification, translation, and smart reply.

Each async call in this chapter returns an com.codename1.util.AsyncResource. Use .ready(…​) for the success path, .except(…​) for errors, and .cancel() to abort an in-flight request. The streaming chat call also dispatches deltas through a StreamingListener; both listener callbacks and ready callbacks fire on the EDT.

63.1. The com.codename1.ai package

The LlmClient class is the single entry point. Static factories return a configured client for each supported provider. Every client speaks the same ChatRequest and ChatResponse value types, so the call site does not change when you swap providers:

// OpenAI (also drives Ollama, vLLM, llama.cpp, and other
// OpenAI-compatible endpoints).
LlmClient openai = LlmClient.openai(apiKey);

// Local Ollama on the default port (http://localhost:11434).
LlmClient ollama = LlmClient.ollama("llama3.2");

// Any OpenAI-compatible endpoint (Together, Groq, Fireworks, vLLM, null).
LlmClient together = LlmClient.localOpenAiCompatible(
        "https://api.together.xyz/v1",
        apiKey,
        "meta-llama/Llama-3.3-70B-Instruct-Turbo");

// Anthropic and Google Gemini, both via their OpenAI-compatible
// endpoints. The wire format is identical; only the base URL,
// default model, and auth differ.
LlmClient anthropic = LlmClient.anthropic(apiKey);
LlmClient gemini = LlmClient.gemini(apiKey);

All five factories return the same LlmClient API. OpenAI, Ollama, vLLM, and llama.cpp share the canonical wire format. Anthropic’s https://api.anthropic.com/v1/chat/completions and Google’s https://generativelanguage.googleapis.com/v1beta/openai/chat/completions implement the same shape, so the framework speaks to all three through one network layer. Default models pick a sensible production-grade target per provider (gpt-4o-mini, claude-sonnet-4-5, gemini-2.0-flash, llama3.2); override per request with ChatRequest.builder().model(…​).

63.1.1. A first chat

ChatRequest is a builder. The bare minimum is a model identifier and one user message:

ChatRequest req = ChatRequest.builder()
        // No model named: the request uses the client's default, which
        // LlmClient#setDefaultModel sets and the simulator's Ollama
        // redirect sets for you. Naming a cloud model here would ask a
        // local server for one it has never heard of.
        .addMessage(ChatMessage.system("Reply in haiku."))
        .addMessage(ChatMessage.user("Describe a Codename One app."))
        .temperature(0.7f)
        .maxTokens(200)
        .build();

openai.chat(req).ready(resp -> {
    Dialog.show("Reply", resp.getText(), "OK", null);
}).except(err -> {
    Log.e(err);
});

ChatResponse.getText() concatenates every text part of the assistant message. getFinishReason() returns one of "stop", "length", "tool_calls", "content_filter", or "error". getUsage() returns prompt, completion, and total token counts when the provider reports them; the fields return -1 when the provider omits them.

63.1.2. Streaming tokens

chatStream(…​) opens an SSE connection and dispatches deltas through a StreamingListener on the EDT. The returned AsyncResource resolves to the final aggregated ChatResponse when the stream completes; calling cancel() on the resource closes the underlying socket:

StringBuilder buffer = new StringBuilder();

// Open the assistant bubble first. appendToLastMessage() writes to
// whichever bubble is newest, which is nothing at all on a fresh view
// and the user's own message once one has been added.
ChatBubble streaming = chatView.beginAssistantStream();

openai.chatStream(req, new StreamingListener.Adapter() {
    @Override
    public void onContentDelta(String delta) {
        buffer.append(delta);
        streaming.appendText(delta);
    }

    @Override
    public void onError(Throwable t) {
        Log.e(t);
    }
}).ready(resp -> {
    // getUsage() is null when the provider omits token accounting, which
    // OpenAI-compatible endpoints commonly do.
    if (resp.getUsage() != null) {
        Log.p("Total tokens: " + resp.getUsage().getTotalTokens());
    }
});

The decoder reassembles fragmented SSE deltas before invoking the listener, so onContentDelta always receives a complete token chunk. Tool-call fragments are reassembled the same way and surfaced through onToolCallDelta(index, id, name, argumentsFragment); name is non-null on the first fragment and id is present on the first call.

63.1.3. Tool calling

Tool describes a callable function with a JSON schema. Pass a ToolHandler to make it executable; the model can then call the tool and the handler returns the JSON result that gets fed back into the conversation:

Tool weather = new Tool(
        "get_weather",
        "Return the current weather for a city.",
        "{\"type\":\"object\",\"properties\":{" +
            "\"city\":{\"type\":\"string\"}}," +
            "\"required\":[\"city\"]}",
        argumentsJson -> {
            Map<String, Object> args = JSONParser.parseJSON(argumentsJson);
            // Serialize rather than concatenate: the city is a value the
            // model produced, and a quote or backslash in it would emit
            // malformed JSON that cannot be sent back as a tool result.
            Map<String, Object> result = new HashMap<String, Object>();
            result.put("tempC", Integer.valueOf(22));
            result.put("city", args.get("city"));
            return JSONParser.toJson(result);
        });

ChatRequest req = ChatRequest.builder()
        // No model named: the client's default applies, which the
        // simulator's Ollama redirect sets for you. Naming a cloud model
        // asks a local server for one it does not have.
        .addMessage(ChatMessage.user("What is the weather in Tel Aviv?"))
        .tools(Collections.singletonList(weather))
        .toolChoice(ToolChoice.AUTO)
        .build();

openai.chat(req).ready(resp -> {
    if (resp.getToolCalls().isEmpty()) {
        // ToolChoice.AUTO lets the model answer without reaching for a
        // tool, and that answer is this response. There is nothing to
        // send back.
        Log.p(resp.getText());
        return;
    }

    // Otherwise the response is the model asking for the tools, not the
    // answer. Run every call it asked for, then send the conversation
    // back once with a result for each: the assistant message names all
    // of them, and a follow-up carrying only some is rejected as an
    // incomplete tool-call sequence.
    ChatRequest.Builder followUp = ChatRequest.builder()
            .addMessage(ChatMessage.user("What is the weather in Tel Aviv?"))
            .addMessage(resp.getAssistantMessage());
    for (ToolCall call : resp.getToolCalls()) {
        try {
            String result = call.execute(Collections.singletonList(weather));
            followUp.addMessage(ChatMessage.toolResult(call.getId(), result));
        } catch (Exception err) {
            // execute() runs your own handler, so it can fail for any
            // reason, and this callback has nowhere to propagate to.
            // The turn still needs a result for this id.
            Log.e(err);
            followUp.addMessage(ChatMessage.toolResult(call.getId(),
                    "{\"error\": \"the tool failed\"}"));
        }
    }
    openai.chat(followUp.build()).ready(answer -> Log.p(answer.getText()));
});

ToolChoice covers the four standard modes:

  • ToolChoice.AUTO — the model picks at will (default).

  • ToolChoice.NONE — the model must not call any tool.

  • ToolChoice.REQUIRED — the model must call exactly one tool.

  • ToolChoice.named("get_weather") — force a specific tool.

63.1.4. Structured output

Constrain a response to JSON with ResponseFormat.JSON_OBJECT. The client adds the provider-specific flag, and ChatResponse.getText() returns a string the runtime can hand to JSONParser:

ChatRequest req = ChatRequest.builder()
        // No model named: the client's default applies, and the
        // simulator's Ollama redirect sets that for you. Naming a
        // cloud model asks a local server for one it lacks.
        .responseFormat(ResponseFormat.JSON_OBJECT)
        .addMessage(ChatMessage.system(
            "Return a JSON object with keys city and population."))
        .addMessage(ChatMessage.user("Tel Aviv"))
        .build();

63.1.5. Multi-modal messages

Attach an image to a user message by adding an ImagePart. The part accepts either inline bytes plus a MIME type or a remote HTTPS URL:

ImagePart photo = new ImagePart(receiptBytes, "image/jpeg");
ChatMessage msg = ChatMessage.userWithImage(
        "Extract the line items from this receipt.", photo);

ChatRequest req = ChatRequest.builder()
        // No model named: the client's default applies, and the
        // simulator's Ollama redirect sets that for you. This request
        // carries an image, so whichever default is in play has to
        // accept one -- the redirect's own default is text-only, so
        // point cn1.ai.ollamaModel at a vision-capable model to run
        // this locally.
        .addMessage(msg)
        .build();

63.1.6. Embeddings

embed(EmbeddingRequest) returns one Embedding vector per input. Use the vectors for semantic search, deduplication, or downstream clustering:

EmbeddingRequest req = EmbeddingRequest.builder()
        // Embeddings need an embedding model, and the client carries a
        // single default that is normally a chat one -- so name it here.
        // Against a local Ollama, name an embedding model you pulled.
        .model("text-embedding-3-small")
        .addInput("a cat sat on the mat")
        .addInput("a feline rested on the rug")
        .build();

openai.embed(req).ready(resp -> {
    float[] v0 = resp.getData().get(0).getVector();
    float[] v1 = resp.getData().get(1).getVector();
    // Compute cosine similarity, persist to Storage, etc.
});

63.1.7. Image generation

ImageGenerator mirrors the LLM client shape. The OpenAI factory drives DALL-E 3 today; the on-device factory routes through the optional cn1-ai-stablediffusion cn1lib (when present in the consumer project):

// Through your own endpoint, as the credentials section requires:
// ImageGenerator.openai(apiKey) would put the billable provider key on
// the device. ImageGenerator.onDevice() is the other option, and needs
// the cn1-ai-stablediffusion cn1lib.
ImageGenerator gen = ImageGenerator.openAiCompatible(
        "https://api.example.com/ai/v1", sessionToken);
GenerateImageRequest req = new GenerateImageRequest(
        "A pastel watercolor of a Tel Aviv beach at sunset");
req.setSize("1024x1024");
req.setQuality("hd");

gen.generate(req).ready(image -> {
    Label preview = new Label(image);
    Display.getInstance().getCurrent().add(preview).revalidate();
});

DALL-E 3 supports count = 1 only. Larger batches require a different underlying model; pass it via setModel(…​).

63.1.8. Conversation persistence

ConversationStore wraps Storage to serialize a list of ChatMessage values to JSON under a named key:

ConversationStore store = new ConversationStore("chat-history");
List<ChatMessage> history = store.load();        // empty list on first call

history.add(ChatMessage.user("Hello"));
ChatResponse resp = openai.chat(
        ChatRequest.builder().messages(history).build())
    .get();                                       // blocking helper, EDT-safe
history.add(resp.getAssistantMessage());
store.save(history);

Tokenizer.estimateMessages(history) returns a rough token count so you can trim the oldest turns before the conversation outgrows the model’s context window.

63.1.9. Prompt templates

PromptTemplate does simple {placeholder} substitution. Unknown placeholders pass through unchanged so partially-filled templates are safe to log:

PromptTemplate t = PromptTemplate.of(
        "Translate the following from {source} to {target}: {text}");
t.put("source", "English");
t.put("target", "French");
t.put("text", "good morning");

ChatMessage user = t.asUser();                    // wraps the rendered string

63.1.10. Retry policy

RetryPolicy.exponentialBackoff() returns a sensible default (four attempts, 500ms initial delay, 30s cap, jitter). Wrap any chat call in a retry loop yourself, or attach the policy to a higher-level wrapper. LlmException.getRetryAfterSeconds() returns the provider’s Retry-After header value when present, or -1, so the policy can honor rate-limit hints.

63.1.11. Simulator redirect for offline development

The JavaSE simulator pings localhost:11434 at startup. When Ollama is running, the simulator exports the flag cn1.ai.ollamaDetected=true. A second system property, cn1.ai.simulatorRedirect, controls whether the simulator transparently routes LlmClient.openai(…​) calls to the local Ollama endpoint instead:

ValueBehavior

disabled

The default on a device. Calls go to the configured provider, even in the simulator.

auto

The default in the simulator. If Ollama is reachable, route OpenAI calls through it. Otherwise behave like disabled.

ollama

Force redirect to Ollama. Use this in offline-only development environments.

The redirect target is also configurable: cn1.ai.ollamaUrl defaults to http://localhost:11434/v1, and cn1.ai.ollamaModel defaults to llama3.2. Unchanged production code therefore runs offline against a local model without any conditional wiring at the call site.

63.1.12. Storing the API key

Never put a provider API key on the device at all — not hard-coded, not in a bundled resource, and not fetched at runtime and cached. Mobile binaries are trivially reverse-engineered, and a key the running app can read is a key its user can extract; SecureStorage protects data at rest and can’t change that. A leaked provider key is billable by whoever holds it, against your account, with no way to scope the damage to one user.

Keep the key on your server and proxy the provider calls through it. The device then authenticates to your endpoint with a short-lived, user-scoped token, which expires on its own and can be revoked for one user without touching anyone else. That token is worth caching with the non-prompting SecureStorage overloads:

// Your server keeps the provider key and proxies the calls. The device
// holds only a short-lived, user-scoped token for your own endpoint, so
// a stolen one expires and can be revoked for that user alone.
SecureStorage store = SecureStorage.getInstance();
boolean saved = store.set("ai.session.token", sessionTokenFromServer);

// Treat the store as a cache, not as the source of truth. When the write
// fails -- no secure storage on this port, or the keychain refused it --
// reading back gives either nothing or the previous, now expired, token.
// The one just issued is the good one, so use it and let persistence be
// best effort.
String token = saved ? store.get("ai.session.token") : sessionTokenFromServer;
if (token == null) {
    token = sessionTokenFromServer;
}
LlmClient client = LlmClient.localOpenAiCompatible(
        "https://api.example.com/ai/v1", token, "gpt-4o-mini");

63.2. The ChatView component

ChatView is a scrollable, theme-aware chat surface that handles the list of bubbles, the streaming append, the typing indicator, and the input strip in one component. Drop it into a Form with a single BorderLayout.CENTER:

ChatView in the JavaSE simulator
Figure 266. ChatView rendered in the JavaSE simulator under the iOS Modern theme

The component exposes thread-safe addMessage, appendToLastMessage, and setTypingIndicatorVisible methods, so background callbacks from chatStream(…​) can mutate the view directly:

Form chat = new Form("Assistant", new BorderLayout());
ChatView view = new ChatView();
chat.add(BorderLayout.CENTER, view);

view.addMessage(ChatMessage.assistant("How can I help?"));

// One request at a time. A second send while the first is still
// streaming builds its request without the first reply, and the two
// completions then append to the history in whatever order they finish,
// producing turns like user A, user B, assistant B, assistant A.
boolean[] sending = {false};

view.setOnSend(e -> {
    if (sending[0]) {
        return;
    }
    sending[0] = true;
    String text = view.getInput().getText();
    view.getInput().clear();
    view.addMessage(ChatMessage.user(text));
    view.setTypingIndicatorVisible(true);

    // Snapshot the history before opening the assistant bubble. The view
    // records streamed text as it arrives, so completed replies are
    // already in there; the one thing to leave out is the empty
    // placeholder that beginAssistantStream() is about to append.
    //
    // No model named: the client's default applies, and the simulator's
    // Ollama redirect sets that for you.
    ChatRequest req = ChatRequest.builder()
            .messages(new ArrayList<ChatMessage>(view.getHistory()))
            .build();

    ChatBubble streaming = view.beginAssistantStream();
    // The proxy client from the credentials section: on a device
    // LlmClient.openai(apiKey) would need the billable provider key to
    // be present there, which that section says not to do.
    client.chatStream(req, new StreamingListener.Adapter() {
        @Override public void onContentDelta(String d) {
            // Append to the bubble this send opened, not to whichever is
            // newest: a second send while this one is still streaming
            // would otherwise divert this response into that bubble.
            streaming.appendText(d);
        }
    }).ready(resp -> {
        sending[0] = false;
        view.setTypingIndicatorVisible(false);
    }).except(err -> {
        // Without this the indicator stays up for good on a failure, and
        // the guard above would block every later send.
        sending[0] = false;
        view.setTypingIndicatorVisible(false);
        Log.e(err);
    });
});
chat.show();

ChatBubble and ChatInput are public, so subclass them for custom rendering. Override ChatView.createBubble(message) to swap in a custom subclass when the view builds the message list.

63.2.1. Theming

ChatView exposes the following UIIDs out of the box:

UIIDApplies to

ChatView

The outer container.

ChatViewMessages

The scrollable message column.

ChatBubbleUser

The container for a user message.

ChatBubbleAssistant

The container for an assistant message.

ChatBubbleSystem

The container for a system message.

ChatBubbleText

The inner TextArea of every bubble.

ChatTypingIndicator

The animated typing dots.

ChatInput

The input strip.

ChatInputField

The text field inside the strip.

ChatSendButton

The send button.

ChatAttachButton

The attach button.

ChatVoiceButton

The voice button.

Style them from theme.css to align with the rest of the app. Hide the voice or attach button by leaving its listener unset; the corresponding Button instance is hidden when no ActionListener is registered.

63.2.2. One-call binding to an LLM

LlmChatBinding.bind(view, client, baseRequest) wires the input bar directly to chatStream(…​). Every send replays the conversation history, dispatches the response into the view, and updates the typing indicator. Use it for prototypes, or as a reference implementation when you need a custom send pipeline:

LlmChatBinding.bind(view,
        // The proxy client from the credentials section, not
        // LlmClient.openai(apiKey): a shipped build must not carry the
        // provider key on the device.
        client,
        // The template's messages are sent ahead of the conversation on
        // every request, so this is where a system prompt belongs.
        // build() also rejects an empty message list.
        ChatRequest.builder()
                .addMessage(ChatMessage.system("You are a helpful assistant."))
                .build());

63.3. Speech recognition and TextToSpeech

The new media APIs route through Display and into the implementation hooks on CodenameOneImplementation, so the call surface is identical on every platform. The default implementation returns false from isSupported() and is a no-op for recognize/speak.

63.3.1. SpeechRecognizer

iOS uses SFSpeechRecognizer, Android uses android.speech.SpeechRecognizer, and the JavaSE simulator stays unsupported unless the optional cn1-ai-whisper cn1lib is on the classpath. Call stop() to end an active session early; partial results stop firing immediately.

63.3.2. TextToSpeech

if (TextToSpeech.isSupported()) {
    TtsOptions opts = new TtsOptions()
            .setLanguageTag("fr-FR")
            .setRate(1.0f);
    TextToSpeech.speak("Bonjour", opts);
}

iOS uses AVSpeechSynthesizer, Android uses android.speech.tts.TextToSpeech, and the JavaSE simulator falls back to say on macOS, espeak on Linux, and the platform SAPI bridge on Windows. getAvailableVoices() returns the platform-specific voice identifiers when the OS exposes them; setVoiceId(…​) accepts any of those strings, or null to use the default voice for the configured language.

63.4. On-device vision

The vision API is three layers deep, and most applications only need the first one. Start with the finished screen, drop to a component when the screen has to look like your own, and drop to a single analyzer when you are working on one image at a time.

63.4.1. Scanning a barcode or QR code

CodeScanner is an entire scanner screen in one call. It opens the camera, decodes, restores the form you came from, and hands back the code. This is the replacement for the cn1-codescan cn1lib:

if (!CodeScanner.isSupported()) {
    ToastBar.showErrorMessage("This device cannot scan codes");
    return;
}

CodeScanner.scan().ready(code -> {
    if (code == null) {
        return;                       // the user backed out
    }
    resultLabel.setText(code.getValue());
}).except(error -> Log.e(error));

The result completes with null when the user backs out — the cn1lib’s scanCanceled() — and fails through except when the camera can’t be opened, which is its scanError(). There is no third callback to implement.

CodeScannerOptions rewords the screen and restricts which symbologies end the scan, so a stray product barcode in the frame doesn’t complete a QR scan:

CodeScanner.scan(new CodeScannerOptions()
        .title("Boarding pass")
        .hint("Hold the pass flat inside the frame")
        .formats(BarcodeFormat.PDF417, BarcodeFormat.AZTEC))
    .ready(code -> {
        if (code != null) {
            checkIn(code.getValue());
        }
    })
    .except(error -> Log.e(error));

The BarcodeFormat constants are the portable symbology names every backend maps onto, so no format literals are needed.

Three classes share the name CodeScanner. Import com.codename1.ai.vision.CodeScanner. The other two are the cn1-codescan cn1lib’s com.codename1.ext.codescan.CodeScanner and the long-deprecated com.codename1.codescan.CodeScanner in core, whose getInstance() returns null on current targets.

63.4.2. Analyzing the live camera

VisionCameraView is a component that owns the camera and runs an analyzer over its frames. It handles the session, the frame conversion, the dropping of frames that inference has fallen behind, and the hop back to the EDT; the application implements one listener. It works with every analyzer, not just barcodes:

FaceDetector detector = new FaceDetector();
VisionCameraView<Face[]> view = new VisionCameraView<>(detector);
view.setFacing(CameraFacing.FRONT);
view.setListener(new VisionPipelineListener<Face[]>() {
    @Override
    public void result(Face[] faces, VisionImage source) {
        shutter.setEnabled(faces.length == 1);
        statusLabel.setText(faces.length == 1
                ? "Looking good"
                : "Fit exactly one face in the frame");
    }

    @Override
    public void error(Throwable error) {
        Log.e(error);
    }
});

Form form = new Form("Selfie", new BorderLayout());
form.add(BorderLayout.CENTER, view);
form.add(BorderLayout.SOUTH, BoxLayout.encloseY(statusLabel, shutter));
form.show();
// The camera is opened when the form is shown and released when the
// user navigates away. Call view.close() when the screen is finished
// with for good, which also releases the detector.

The analyzer is constructed by the application rather than named by a feature constant. That’s deliberate: the builders decide which native vision dependency to package from the analyzer classes an application references, so building the analyzer in your own code is what keeps a face-detection app from also carrying the barcode and pose models.

The camera opens when the view is shown and is released when the user navigates away, so a form transition can’t leave the hardware held. close() additionally releases the analyzer, for a screen that’s finished with for good.

setScaleType(…​), and the mirroring a front camera asks for, are honored in the simulator. The iOS and Android previews render fixed at fill-and-crop and unmirrored, because neither port implements the preview-scaling hook on CameraImpl yet. Treat both as requests rather than guarantees on a device.
The preview is a native view, and Codename One components can’t be painted over it on every platform — iOS renders native peers behind the Codename One layer, Android renders them in front. Put a reticle, hints, and buttons around the preview rather than on top of it. Result geometry can still be drawn over a still image, as below.

63.4.3. The analyzers

Each analyzer takes a VisionImage and returns a typed result. Create one, reuse it across a sequence of images, and close it when finished. Check isSupported() before offering the feature: availability depends on the target, the linked backend, and the OS version.

VisionImage accepts JPEG, PNG, NV21, or RGBA8888 data. VisionImage.fromFile(path) reads a gallery or captured photo, VisionImage.fromImage(image) takes one already in memory — passing an EncodedImage’s original bytes straight through rather than re-encoding — and `VisionImage.fromCameraFrame(frame) copies a live frame’s callback-owned buffers and preserves its rotation.

BarcodeScanner decodes codes from a still image:

BarcodeScanner scanner = new BarcodeScanner();
scanner.process(VisionImage.fromFile(photoPath)).ready(codes -> {
    for (Barcode code : codes) {
        if (BarcodeFormat.matches(code, BarcodeFormat.EAN_13,
                BarcodeFormat.UPC_A)) {
            lookUpProduct(code.getValue());
        }
    }
    scanner.close();
}).except(error -> {
    Log.e(error);
    scanner.close();
});

TextRecognizer reads text. Over a file:

TextRecognizer recognizer = new TextRecognizer();
recognizer.process(VisionImage.fromFile(photoPath)).ready(result -> {
    textArea.setText(result.getText());
    recognizer.close();
}).except(error -> {
    Log.e(error);
    recognizer.close();
});

Or from the live camera, stopping at the first block that looks like the value being hunted for:

VisionCameraView<TextRecognitionResult> view =
        new VisionCameraView<>(new TextRecognizer());
view.setListener(new VisionPipelineListener<TextRecognitionResult>() {
    @Override
    public void result(TextRecognitionResult text, VisionImage source) {
        for (TextRecognitionResult.TextBlock block : text.getBlocks()) {
            // Every frame is analyzed until one block looks like the
            // value being hunted for.
            if (block.getText().startsWith("SN-")) {
                accept(block.getText());
                return;
            }
        }
    }

    @Override
    public void error(Throwable error) {
        Log.e(error);
    }
});

FaceDetector returns bounds, head angles, and named landmarks. Cropping a photo to the face it contains:

Image photo = EncodedImage.create(jpegBytes);
FaceDetector detector = new FaceDetector();
detector.process(VisionImage.fromImage(photo)).ready(faces -> {
    if (faces.length > 0) {
        Rectangle box = faces[0].getBounds()
                .toBounds(0, 0, photo.getWidth(), photo.getHeight());
        preview.setIcon(photo.subImage(box.getX(), box.getY(),
                box.getWidth(), box.getHeight(), true));
    }
    detector.close();
}).except(error -> {
    Log.e(error);
    detector.close();
});

Read individual landmarks with face.getLandmark(…​) and the FaceLandmarks constants. Not every backend fills in every field: getSmilingProbability() and getTrackingId() are negative when unavailable, and Apple Vision reports neither.

ImageLabeler classifies what an image contains:

ImageLabeler labeler = new ImageLabeler(new VisionOptions()
        .minimumConfidence(0.6f)
        .maximumResults(5));

labeler.process(VisionImage.fromFile(photoPath)).ready(labels -> {
    StringBuilder summary = new StringBuilder();
    for (ImageLabel label : labels) {
        summary.append(label.getText())
               .append(" (")
               .append(Math.round(label.getConfidence() * 100))
               .append("%)\n");
    }
    resultLabel.setText(summary.toString());
    labeler.close();
}).except(error -> {
    Log.e(error);
    labeler.close();
});

The label text is the portable part. ImageLabel.getIndex() is a model class index that differs between backends and is -1 on Apple Vision, so branch on getText().

PoseDetector locates body joints. Backends detect different subsets of the skeleton, so look joints up by name and handle a missing one:

VisionCameraView<Pose> view = new VisionCameraView<>(new PoseDetector());
view.setListener(new VisionPipelineListener<Pose>() {
    private boolean wasUp;

    @Override
    public void result(Pose pose, VisionImage source) {
        Pose.Landmark wrist = pose.getLandmark(PoseLandmarks.RIGHT_WRIST);
        Pose.Landmark shoulder =
                pose.getLandmark(PoseLandmarks.RIGHT_SHOULDER);
        if (wrist == null || shoulder == null
                || wrist.getConfidence() < 0.6f) {
            return;                    // this frame did not see the arm
        }
        // Y grows downwards, so "above" is the smaller value.
        boolean up = wrist.getPosition().getY()
                < shoulder.getPosition().getY();
        if (up && !wasUp) {
            reps++;
            statusLabel.setText(String.valueOf(reps));
        }
        wasUp = up;
    }

    @Override
    public void error(Throwable error) {
        Log.e(error);
    }
});

SelfieSegmenter separates a person from the background. SegmentationMask.cutOut(…​) applies the mask to the source image and rescales it, since the mask has the model’s resolution rather than the frame’s:

EncodedImage photo = EncodedImage.create(jpegBytes);
SelfieSegmenter segmenter = new SelfieSegmenter();

segmenter.process(VisionImage.encoded(jpegBytes)).ready(mask -> {
    // Below 60% foreground confidence becomes transparent, so whatever
    // is painted behind this image shows through.
    Image person = mask.cutOut(photo, 0.6f);
    preview.setIcon(person);
    segmenter.close();
}).except(error -> {
    Log.e(error);
    segmenter.close();
});

SegmentationMask.toMaskImage(rgb) renders the mask itself as a translucent tint, for highlighting the subject rather than cutting it out.

DocumentScanner flattens a photographed page:

DocumentScanner scanner = new DocumentScanner();
if (!scanner.isSupported()) {
    // Google's Android document scanner is an interactive flow rather
    // than an analyzer, so Android reports this unsupported.
    scanner.close();
    return;
}
scanner.process(VisionImage.fromFile(photoPath)).ready(result -> {
    List<Image> pages = new ArrayList<>();
    for (int i = 0; i < result.getPageCount(); i++) {
        pages.add(EncodedImage.create(result.getPage(i)));
    }
    show(pages);
    scanner.close();
}).except(error -> {
    Log.e(error);
    scanner.close();
});

63.4.4. Drawing the results

Bounds and points are normalized to the range 0 through 1 rather than pixels, so one observation maps onto an image of any size. VisionRect.toBounds(component) and VisionPoint.toPoint(component) convert them into the absolute coordinates a paint method draws in:

class FaceOverlay extends Label {
    private Face[] faces = new Face[0];

    FaceOverlay(Image photo) {
        super(photo);
    }

    void setFaces(Face[] value) {
        faces = value;
        repaint();
    }

    @Override
    public void paint(Graphics g) {
        super.paint(g);
        g.setColor(0x34c759);
        for (Face face : faces) {
            // Results are normalized to 0..1, so the same observation maps
            // onto whatever size this component happens to be.
            Rectangle r = face.getBounds().toBounds(this);
            g.drawRect(r.getX(), r.getY(), r.getWidth(), r.getHeight(), 3);
        }
    }
}

The mapping stretches the whole normalized range across the destination, which is right when the destination has the same aspect ratio as the analyzed image. A live preview scaled with ScaleType.CROP doesn’t, which is the other half of why overlays belong on still images.

63.4.5. Scripting results in the simulator

The desktop has no on-device vision models. Rather than report every analyzer as unsupported — which would leave a scanner screen impossible to build without a device — the simulator returns whatever you script under Simulate > Vision:

  • Vision Supported turns the whole API off, so the isSupported() branch can be exercised.

  • Analysis outcome switches between the scripted result, an empty result, VisionException.UNSUPPORTED, and a backend error. Every screen needs the "nothing found" and "it failed" paths, and this is how to reach them.

  • The remaining items set the scripted values: the decoded code and its format, the recognized text, the image labels, how many faces are found and whether they’re smiling, and whether a body pose is detected.

The scripted results carry plausible geometry, so overlay and debounce code can be written and debugged before the app ever reaches a device. Nothing in this section applies to a device build.

63.4.6. Backends and options

An image whose stored pixels aren’t already upright needs its rotation declared: VisionImage.encoded(bytes, rotationDegrees). The one-argument overload, and fromFile(…​), assume the pixels are upright and don’t inspect EXIF metadata; fromCameraFrame(…​) takes the rotation from the frame. VisionOptions.minimumConfidence(…​) clamps finite and infinite values to the range 0 through 1 and rejects NaN.

Android uses ML Kit. iOS and Mac native builds default to Apple Vision and Core Image. iOS can select ML Kit explicitly:

VisionOptions options = new VisionOptions()
        .backend(VisionBackends.mlKitTextRecognition())
        .minimumConfidence(0.5f);

new TextRecognizer(options)
        .process(VisionImage.encoded(jpegBytes))
        .ready(result -> Log.p(result.getText()))
        .except(error -> Log.e(error));

63.4.7. Reading non-Latin text

TextRecognizer reads the Latin script by default. Select a writing system with VisionOptions.textScript(…​) to read Chinese, Devanagari, Japanese, or Korean:

TextRecognizer recognizer = new TextRecognizer(new VisionOptions()
        .textScript(TextScript.japanese()));

recognizer.process(VisionImage.encoded(jpegBytes))
        .ready(result -> Log.p(result.getText()))
        .except(error -> Log.e(error));

The selector names a script rather than a language, because that’s what the underlying recognizers are organized by. One script covers every language written in it, and each script model also reads the Latin characters mixed into the same page. Recognize one script per analyzer; create a second analyzer when a screen needs both.

Android maps each script to the matching ML Kit recognizer. Apple platforms map it to the Vision recognition languages for that script, asking the OS which languages it supports rather than assuming a list. When the OS recognizes none of them, the analysis fails with VisionException.UNSUPPORTED rather than returning Latin OCR of the page. Select VisionBackends.mlKitTextRecognition() to use ML Kit’s model for that script on iOS instead.

Each script is a separate model, and referencing TextScript.japanese() is what adds it: the ML Kit script bundle on Android, and on iOS the matching pod when the ML Kit backend was also selected. An application that never names a script carries only the Latin model, and one that names Japanese doesn’t carry the Korean or Devanagari models.

On an iOS simulator, the Apple barcode backend uses Vision barcode request revision 1. Newer simulator runtimes can otherwise complete a barcode request without reporting valid QR codes. Physical devices use the latest revision supplied by the OS. Test newer symbologies such as Micro QR on a device, or select the matching ML Kit backend.

Create and reuse an analyzer when processing a sequence. Call close() when the analyzer is no longer needed. Before showing a feature, call its isSupported() method: availability can depend on the target, linked backend, and OS version.

VisionPipeline attaches an analyzer to a CameraSession frame stream and keeps only the newest pending frame. Live OCR and pose detection therefore can’t build an unbounded queue when inference is slower than the camera. While an analysis is busy, the pipeline reuses the pending buffer when newer same-sized frames replace it. Results and errors are delivered on the Codename One EDT.

CameraFrame.getRawBytes() can be null even after requesting NV21 or RGBA8888 when a port can’t expose that format. JPEG bytes remain available; VisionImage.fromCameraFrame(…​) automatically selects that fallback.

DocumentScanner currently performs still-image rectangle correction on Apple platforms. Google’s Android document scanner is an interactive camera flow rather than an analyzer for an existing VisionImage, so the Android backend reports it as unsupported rather than launching a UI flow.

63.4.8. Vision feature and dependency selection

The Java package is always part of core. The builders scan the concrete analyzer classes and retain only the native adapter and dependency for each class the application actually uses:

APIAndroidApple default / optional backend

TextRecognizer

ML Kit text recognition

Vision / GoogleMLKit/TextRecognition

TextScript (non-Latin)

ML Kit bundle for that script

Vision recognition languages / GoogleMLKit/TextRecognition<Script>

BarcodeScanner, CodeScanner

ML Kit barcode scanning

Vision / GoogleMLKit/BarcodeScanning

FaceDetector

ML Kit face detection

Vision / GoogleMLKit/FaceDetection

ImageLabeler

ML Kit image labeling

Vision / GoogleMLKit/ImageLabeling

PoseDetector

ML Kit pose detection

Vision / GoogleMLKit/PoseDetection

SelfieSegmenter

ML Kit selfie segmentation

Vision / GoogleMLKit/SegmentationSelfie

DocumentScanner

Unsupported for still images

Core Image perspective correction

CodeScanner selects the same barcode dependency BarcodeScanner does, because it builds one internally; an application that references only the ready-made screen still gets the decoder. CodeScanner and VisionCameraView additionally select the camera natives, so neither has to be paired with a com.codename1.camera reference to work.

The optional iOS ML Kit pod is added only when both the analyzer and its feature-specific selector are referenced. For example, VisionBackends.mlKitBarcodeScanning() adds only the barcode pod. Current Google ML Kit iOS pods require iOS 15.5. Selecting one of these optional backends automatically raises the effective application and CocoaPods deployment target to 15.5 when the configured target is lower. Referencing one Android analyzer doesn’t compile or package the other five adapters.

63.5. Portable LiteRT inference

InferenceSession loads .tflite data from bytes, a bundled resource, or a private file. Android uses LiteRT and can request NNAPI. iOS uses TensorFlow Lite Objective-C and may request the Core ML delegate. Both mobile implementations fall back to CPU unless allowFallback(false) is set. Android and iOS reject strict Accelerator.NPU sessions: NNAPI can mix delegated and CPU operations, and the Core ML delegate can schedule work across the Neural Engine, GPU, and CPU. Neither reports that the entire graph ran on the requested NPU. With fallback enabled, NPU selection is a best-effort accelerator preference. On iOS, strict Accelerator.CORE_ML sessions are also rejected because the delegate doesn’t report whether unsupported model operations remained on LiteRT’s CPU path. The JavaSE simulator and targets without a native LiteRT runtime, including Mac native, return false from InferenceSession.isSupported().

InferenceSession.open(
        ModelSource.resource("/models/classifier.tflite"),
        new InferenceOptions().accelerator(
                InferenceOptions.Accelerator.AUTO))
    .ready(session -> {
        Tensor input = Tensor.floats("serving_default_input",
                new int[] {1, 224, 224, 3}, pixels);
        session.run(new Tensor[] {input})
                .ready(outputs -> {
                    try {
                        consume(outputs[0]);
                    } finally {
                        session.close();
                    }
                })
                .except(error -> {
                    try {
                        Log.e(error);
                    } finally {
                        session.close();
                    }
                });
    })
    .except(error -> Log.e(error));

Open is asynchronous because model initialization can allocate native resources. A session is reusable and supports multiple named inputs, multiple outputs, and resizeInput(…​). A session serializes its mutable native interpreter: while run(…​) is pending, another run, input resize, or input/output metadata query throws IllegalStateException. Tensor data is copied across the port boundary. Always close the session.

Bundle small models as resources. For larger models use ModelCache.fetch(url, cacheKey, sha256), which downloads over HTTPS, verifies the supplied SHA-256 digest, and reuses the app-private cached file. The initial URL must use HTTPS. Ports that expose redirect responses reject a redirect to HTTP and discard the temporary download. iOS follows redirects below the portable network layer, so iOS requires the SHA-256 argument and rejects the unpinned two-argument overload. Supply the digest on every platform for every third-party or remotely mutable model. Interrupted downloads restart through a temporary .download file and are never promoted to the cache until verification succeeds. Concurrent calls for the same cache key and content identity share one download but receive independent asynchronous resources. Canceling one caller suppresses only that caller’s notification; it doesn’t cancel the shared download or the other callers. When a cache key already has an active download, a request with a conflicting URL or digest fails instead of sharing the temporary file.

63.6. On-device language services

LanguageIdentifier, Translator, and SmartReply share LanguageOptions. Its minimum-confidence threshold clamps to 0 through 1 and rejects NaN. Translation models are downloaded and cached by ML Kit when a language pair is first used. Android and iOS builders select language-id, translate, and smart-reply independently, so using language identification alone doesn’t package the other two SDKs.

LanguageOptions options = new LanguageOptions()
        .backend(LanguageBackends.mlKitTranslation());
if (Translator.isSupported(options)) {
    Translator.translate("Where is the station?", "en", "fr",
            options)
        .ready(value -> Log.p(value))
        .except(error -> Log.e(error));
}

LanguageBackends.auto() selects ML Kit on Android and Apple’s Natural Language framework for language identification on iOS. The Apple default keeps arm64 simulator builds native and adds no third-party dependency. LanguageBackends.mlKitLanguageIdentification() opts identification into the ML Kit iOS pod. iOS translation and Smart Reply are explicit opt-ins: use LanguageBackends.mlKitTranslation() or LanguageBackends.mlKitSmartReply() respectively. Merely referencing Translator or SmartReply doesn’t add a pod or exclude the arm64 simulator. Selecting one of those ML Kit backends requires the x86_64 simulator slice and raises an iOS deployment target below 15.5.

The static language methods are convenient for one operation and close their temporary backend after either success or failure. For repeated work, call LanguageIdentifier.open(…​), Translator.open(…​), or SmartReply.open(…​), reuse the returned feature session, and close it when the last pending operation completes. Reusable sessions retain backend state and, on Android, cache ML Kit clients and translation clients per language pair. Mac native and the other unsupported targets return false from the language service isSupported() checks. Completion and failure callbacks arrive on the EDT.

63.7. Platform availability

The public API and the unsupported fallback are present on every Codename One target. Vision has native implementations on Android, iOS, and macOS; Apple Vision is the default on Apple targets and Android uses ML Kit. Language services and LiteRT inference have native implementations on Android and iOS. JavaSE, JavaScript, native Windows, native Linux, watchOS, and tvOS report unsupported for these new APIs; Mac native reports unsupported for language and LiteRT. No target sends data to a cloud inference service. Large portable runtimes or model payloads for the remaining targets remain appropriate opt-in libraries.

No build hints or cn1lib coordinates are required for the built-in mobile implementations. The local Maven plugin and BuildDaemon use the same PlatformFeatureCatalog, so local and cloud builds make the same dependency decisions.

63.8. Migrating from the AI cn1libs

The old com.codename1.ai.mlkit.* and com.codename1.ai.tflite packages aren’t compatibility aliases. Remove those dependencies and move directly to com.codename1.ai.vision, com.codename1.ai.language, or com.codename1.ai.inference. Result coordinates are normalized floats rather than packed platform-specific integer arrays. cn1-ai-whisper and cn1-ai-stablediffusion remain cn1libs because their native payloads and models are intentionally large.

Remove the retired cn1lib dependency before adding the built-in class. During migration, the builders deduplicate identical CocoaPods names and preserve an explicitly declared user version constraint. Removing the old library is still required because compatibility aliases would keep its old native-interface and packaging contracts alive.

63.8.1. Putting it all together

The pieces compose well. The following loop captures a photo, extracts text with the multi-modal gpt-4o model, speaks the result, and streams the same text into a ChatView:

Capture.capturePhoto(evt -> {
    // A cancelled capture arrives as a null event or a null source
    // depending on the port, and reading either would fail rather than
    // simply returning the user to the app.
    if (evt == null || !(evt.getSource() instanceof String)) {
        return;
    }
    String path = (String) evt.getSource();
    byte[] bytes = readAllBytes(path);
    if (bytes == null || bytes.length == 0) {
        // Nothing to describe. Sending an empty ImagePart produces a
        // data URL with no payload, which the provider rejects.
        return;
    }
    ImagePart img = new ImagePart(bytes, "image/jpeg");

    ChatRequest req = ChatRequest.builder()
            // No model named here: the request uses whatever default the
            // client carries. Whichever it is, it has to accept images.
            .addMessage(ChatMessage.userWithImage(
                "Describe the photo in one sentence.", img))
            .build();

    chatView.addMessage(req.getMessages().get(0));
    ChatBubble streaming = chatView.beginAssistantStream();
    StringBuilder full = new StringBuilder();

    // The same proxy client as the credentials sample above: on a device
    // LlmClient.openai(apiKey) would need the billable provider key to be
    // present here, which is exactly what that section says not to do.
    client.chatStream(req, new StreamingListener.Adapter() {
        @Override public void onContentDelta(String d) {
            full.append(d);
            // Append to the bubble this capture opened: a second photo
            // started mid-stream would otherwise take this text.
            streaming.appendText(d);
        }
    }).ready(resp -> {
        TextToSpeech.speak(full.toString());
    });
});

This sample sends an image, so whichever model the client reaches has to accept one — for a shipped app, whatever your proxy is configured with.

Note that the simulator’s Ollama redirect doesn’t apply here. It wraps the public-provider factories, and localOpenAiCompatible returns its client directly, which is the point: a proxy is a real endpoint and isn’t something to swap out behind your back. To try this against a local model, build the client with LlmClient.ollama(…​) and name a vision-capable model you’ve pulled.

The photo leaves the device either way. Nothing here runs a model on the phone, so cover that in your privacy policy — and note that the client is the proxy client from the previous section, not LlmClient.openai(apiKey), because that one needs the billable provider key to be present on the device.

64. MCP Headless API

The Model Context Protocol (MCP) headless API lets an application expose itself to a large language model agent. An agent such as Claude Desktop, Claude Code, Codex, or opencode can read the current screen, drive the user interface, and call tools the application publishes.

The API reuses the accessibility semantics tree. The same immutable tree that describes the screen to VoiceOver and TalkBack also describes it to the agent, so any application is drivable without extra code. Application specific data and actions are exposed through the existing com.codename1.ai.Tool contract.

The socket transport runs anywhere the port can bind the loopback interface, which includes an application running on a device, so an agent can attach to a build on a phone as well as to the simulator. It’s blocked on a release build; see Development builds only. The stdio transport needs process standard input and so is supported by the JavaSE port, which powers the simulator and the desktop tooling.

64.1. The MCP menu

Every desktop Codename One tool, including the simulator and Codename One Settings, gets a native MCP pull-down menu. The menu is added by the JavaSE port, so a tool gets it without any code of its own. It’s the main way a user turns a tool into something an agent can drive.

The menu has these items:

  • Expose This Tool To Agents starts and stops the loopback MCP server for the running tool.

  • Install in MCP Hosts registers this tool with the MCP hosts detected on the machine: Claude Desktop, Claude Code, and Codex. After a host restarts, the tool appears as an MCP server the agent can use. A detected host whose configuration format isn’t supported yet is reported rather than written to.

  • Remove From MCP Hosts removes that registration.

  • Detect MCP Hosts lists the hosts found on the machine and where their configuration lives.

  • Debug Logging controls how much of the MCP conversation is echoed to the log.

Install writes a command that launches MCPStdioLauncher, a small bridge that relays the host’s standard input and output to the running tool’s socket. Hosts speak stdio and the tool serves a socket, so the bridge lets a stdio host such as Claude Desktop drive the already-running, human-visible tool.

64.2. Starting the server

Invoking the API is the switch. There is no build hint or property to set. The socket server lets an agent attach to a session a person is watching, which is the usual choice in the simulator:

MCP.startSocketServer(8765);

A tool that a host launches as a subprocess can serve over standard input and output instead:

MCP.startStdioServer();

The stdio transport is the standard MCP local transport, exchanging newline delimited JSON-RPC messages. While it runs, application logging is redirected away from standard output so it can’t corrupt the protocol stream. The stdio transport lives in the JavaSE port because it needs process standard input, which isn’t available on every target.

64.3. Attaching to an application on a device

The socket transport binds the loopback interface, and it binds the loopback interface of whatever machine the application is running on. In the simulator that’s your own, so an agent connects to 127.0.0.1 and there’s nothing else to arrange. On a phone the port belongs to the phone, and reaching it needs a forward.

On Android, adb forward tcp:8765 tcp:8765 maps a port on the development machine onto the same port inside the device, so an agent connects to 127.0.0.1:8765 as before. Remove it with adb forward --remove tcp:8765 when the session ends. The cn1:android-on-device-debugging goal already installs and launches a debuggable build, and its waitForAttach=false option is the one to use when the point of the session is the agent rather than a breakpoint, since the application then boots straight into a drivable state instead of blocking for a debugger. On iOS the same setting isn’t a convenience but a requirement, because ios.onDeviceDebug.waitForAttach defers the callback that boots the application until a debugger attaches — so a starter in the application’s own code never runs, and there is nothing listening on the port to reach.

The native iOS simulator shares the host’s network stack, so the application’s loopback port is the host’s loopback port and no forwarding applies. A physical iPhone doesn’t: the port sits on the device’s own loopback, reachable only over the USB multiplexing channel, and iproxy from libimobiledevice is the usual relay. Codename One ships no goal for it, so an agent-driven session against a real iPhone depends on external tooling in a way the Android one doesn’t.

None of this is a second security boundary. A forward is something you asked for, and the reason the server refuses a release build is that the loopback interface on a device is shared with every other application installed on it — see Development builds only.

64.4. Development builds only

The socket server refuses to bind on a release build and throws an IllegalStateException instead.

The reason is what an attached agent can do. It reads the screen and drives the user interface, and the loopback interface is shared by everything on the device rather than being private to one application. On a phone that means any other installed application can connect to the port and drive yours. That’s a reasonable trade while you’re developing, because it’s how an agent attaches to a running build, and a poor one in an application a user installs.

A development build is a debuggable Android package or a development provisioned iOS build. Any other target counts as a release build, so a port that can’t tell the difference withholds the server rather than exposing it.

The JavaSE port is the exception, and it’s worth understanding before you rely on this. That port runs the simulator, the designer and the desktop tooling, and it can’t distinguish those from a desktop application packaged for distribution: there’s no debuggable flag and no provisioning profile to read. It reports a development build in every case, because reporting otherwise would take the MCP menu away from every desktop tool. A packaged desktop application is therefore not gated by this, and one that needs to withhold the server from its own release build has to supply that signal itself.

Nothing binds a port unless the application calls a starter, and an application that never calls one has no server at all. The gate matters for the case where a starter is left in the code by accident, which is the way a debugging facility usually reaches production.

The same distinction is available to application code, so a starter can be left in place and simply not fire in a shipped build:

if (Display.getInstance().isDebuggableBuild()) {
    MCP.startSocketServer(8765);
}

A build that ships to devices you control, such as a kiosk fleet, a test lab, or a managed enterprise deployment, can lift the block:

MCP.setAllowOnReleaseBuilds(true);
MCP.startSocketServer(8765);

Treat that call the way you’d treat any other decision to expose a control surface, because the device itself becomes the boundary.

64.5. Built-in user interface tools

Every server registers a small set of tools that read and drive the screen through the accessibility tree. An agent calls ui_snapshot to get the semantics tree of the current form as JSON, including the identifier, role, label, value, and state of each node, and the identifiers of the actions each node supports.

The agent drives the screen with ui_perform_action, which performs an action such as activate or setText on a node from the snapshot. The convenience tools ui_activate and ui_set_text wrap the common cases, and ui_find locates nodes by identifier, by label, or by screen coordinate.

Actions run on the Codename One EDT, so an agent never touches the live component tree directly. Each action returns whether it succeeded together with a fresh snapshot.

64.6. Publishing application tools

An application publishes its own data and actions as MCP tools. A Tool has a name, a description, a JSON schema for its parameters, and a handler:

MCP.addTool(new Tool(
        "current_user",
        "Returns the signed in user",
        "{\"type\":\"object\",\"properties\":{}}",
        new ToolHandler() {
            @Override
            public String invoke(String argumentsJson) {
                return "{\"name\":\"" + signedInUser + "\"}";
            }
        }));

The server merges these tools with the built-in tools when a host lists the available tools, and routes each call to the matching handler. The same Tool type is used by the Codename One AI client, so a tool defined once serves both a hosted agent and an in-application model.

64.7. Screenshots and the simulator

The server exposes a screenshot of the current form as an MCP image resource, so a vision capable model can see the screen alongside the semantic tree. The resource is enabled by default and can be disabled per server.

The JavaSE simulator drives the same MCP menu described above, so an application can be exposed and driven from the running simulator during development.

64.8. Debug logging

Because an agent drives the tool on its own, it helps to watch what it does. The server echoes the MCP conversation to the Codename One log at a level chosen through the menu’s Debug Logging item or in code:

MCP.setVerbosity(MCPVerbosity.FULL);

The levels of MCPVerbosity are, from quietest to loudest: OFF, ERRORS (only failed calls), SUMMARY (one line per call), and FULL (every request and response). Each level includes the one below it.

64.9. Registering with installed hosts

The Install and Remove items in the MCP menu call MCPClientRegistrar, which detects the MCP hosts installed on the machine and writes a server entry into each host configuration, so an end user doesn’t edit configuration by hand. Detection and registration are available to any Codename One tool, and they run inside the runtime because they use the portable FileSystemStorage.

A caller describes the server with an MCPClientDescriptor (a name and the command that launches it) and registers it with the detected hosts. Hosts whose configuration format isn’t yet supported are reported so the user can add the entry manually. opencode is the one that’s left: it nests its servers in an mcp block whose entries have a shape of their own.

Claude Desktop and Claude Code keep their servers in a JSON mcpServers object. Codex keeps its own in ~/.codex/config.toml, as one [mcp_servers.<name>] table per server, and that file is shared by the ChatGPT desktop app, the Codex CLI, and the Codex IDE extension, so registering once reaches all three.

The default path is the one that’s used. If you’ve moved your Codex configuration with CODEX_HOME, add the entry there by hand: the registrar runs inside the Codename One runtime, which has no System.getenv, so it can’t follow the variable.

The TOML file is edited as text rather than parsed and written back. Only the lines belonging to the one server are replaced, so every other server, setting, comment, and choice of formatting in the file survives the edit exactly as the user left it, and the file keeps its own line endings. Registering again replaces the entry instead of adding a second one, and removing takes the server’s env sub-table with it.

A configuration the editor can’t make sense of is left untouched, and the reason is logged. That covers a file that isn’t valid TOML, and the shapes it won’t rewrite: the server declared through a dotted key or an inline table, mcp_servers declared as an array of tables, or the same server declared twice. Losing a Codex configuration is a worse outcome than not registering.

The host reads its configuration at startup, so a host that’s already running needs a restart before the new server appears.

Security and identity

65. Security

Security is a "big word." It implies many things and that’s also true for the mobile app development so before you get started lets try to define the scope of security.

You will deal with application security and its communication mechanisms while ignoring everything beyond that scope. Start with a simple fact:

Codename One applications are secure on the devices by the nature of the mobile OS security.

For most intents and purposes this will be enough, unless you’re specifically concerned about security this section isn’t for you. Mobile OSes isolate applications from one another so it’s hard for an application to damage the OS or even damage/spy on a different app.

The restrictions laid on apps are here to make them extra secure and on top of that Codename One lays a few big advantages in security:

  • Codename One code is compiled (unlike for example, PhoneGap/Cordova)

  • Android release builds are obfuscated by default with R8, which makes the binaries harder to reverse engineer. On the other ports the shipped code is compiled or translated (the iOS/native ports go through ParparVM to C) rather than renamed. Enterprise accounts can turn on cross-platform hardening — name obfuscation, string encryption and more — for every port; see the App Hardening chapter

  • Codename One compiles the UI to native code too which means typical reverse engineering code will have a harder time following

  • Codename One disables debug flags so a hacker won’t be able to debug your production app on the device

Despite that you still need to keep in mind that the binary could still be reverse engineered and so it’s important to avoid storing keys in the client side code. For example, if you have an API key to access a service (for example, Google Cloud key) it needs to be stored in your server and not as a constant in your app!

Each section below discusses some attack vectors against applications and how they can be stopped. Pretty much all these attacks require a sophisticated attacker which will exist for high value targets (for example, bank apps, government etc.).

The sections that follow work through the places a value can be kept. They aren’t interchangeable and they aren’t a single ranking: each one answers a different attacker, the first two barely answer any, and the last two compose rather than compete — Secrets caches what it fetches in SecureStorage. A per-user token belongs in the keychain; an app-wide key you want to rotate without a release belongs in the vault.

Where a secret can live and what each place protects

65.1. Constant obfuscation

One of the first things a hacker will do when compromising an app is look at it. For example, an attacker wanting to exploit a bank’s login UI would look at the label next to the login and then search for it in the decompiled code. If the UI has the String "enter username and password" they can search for that.

It won’t lead directly to a hack or exploit, but it will narrow down the area of the code where you should look and it makes the first step that much easier. Obfuscation helps as it removes descriptive method names but it can’t hide the Strings you use in constants. If an app has a secret key within obfuscating it can make a difference (albeit a slight difference).

Notice that this is a temporary roadblock as any savvy hacker would compile the app and connect a debugger eventually (although this is blocked in release builds) and would be able to inspect values of variables/flow. But the road to reverse engineering the app would be harder even with a simple xor obfuscation.

This isn’t encoding or encryption — it’s a simple obfuscation of the data.

The Util class has two static methods for it: xorEncode(String) turns a string into the obfuscated form, and xorDecode(String) turns it back.

They use a simple xor based obfuscation to make a String less readable. For example, if you’ve code like this:

private static final String SECRET = "Don't let anyone see this/* omitted */";

You might be concerned about the secret, then this would make it slightly harder to find out:

// Don't let anyone see this/* omitted */

Notice that this isn’t secure, if you have a crucial value that must not be found you need to store it in the server. No alternative exists because everything that’s sent to the client can be compromised by a determined hacker.

Use the comment to help you find the string in the code

Codename One’s built-in user-specific constants are obfuscated with this method. For example, an app built with Codename One carries some internal data such as the user who built the app, which is now obfuscated. The following small app encodes strings so you can copy and paste them into your app:

Form hi = new Form("Encoder", BoxLayout.y());
TextField bla = new TextField("", "Type Text Here", 20, TextArea.ANY);
TextArea encoded = new TextArea();
SpanLabel decoded = new SpanLabel();
hi.addAll(bla, encoded, decoded);
bla.addDataChangedListener((a, b) -> {
    String s = bla.getText();
    String e = Util.xorEncode(s);
    encoded.setText(e);
    decoded.setText(Util.xorDecode(e));
    hi.getContentPane().animateLayout(100);
});

hi.show();

This allows you to type in the first text field and the second text area shows the encoded result. A text area is used so copy/paste is easy.

65.2. Storage encryption

Codename One had support for bouncy castle encryption for quite a while but it isn’t as intuitive as you’d like it to be. This makes securing/encrypting your app more painful than it should.

Codename One supports full encryption of the Storage (notice the distinction, Storage isn’t FileSystemStorage). This is available by installing the bouncy castle cn1lib from the extensions menu then using one line of code

You would want that code within your init(Object) method

Notice that you can’t use storage or preferences to store this data as it would be encrypted (Preferences uses Storage internally). You can use a password for this key and it would make it way more secure but if a user changes his password you might have a problem. In that case you might need the old password to migrate to a new password.

This works through a new mechanism in storage where you can replace the storage instance with another instance using:

Storage.setStorageInstance(new MyCustomStorageSubclass());

You can leverage that knowledge to change the encryption password on the encryption storage. The key is installed for the whole of Storage, so every entry has to move together: read them all under the old key, install the new one, write them all back. EncryptedStorage comes from the bouncy castle cn1lib, so this listing isn’t part of the compiled examples:

EncryptedStorage.install(oldKey);
Map<String, byte[]> all = new LinkedHashMap<>();
for (String name : Storage.getInstance().listEntries()) {
    try (InputStream is = Storage.getInstance().createInputStream(name)) {
        all.put(name, Util.readInputStream(is));
    }
}

EncryptedStorage.install(newKey);
for (Map.Entry<String, byte[]> e : all.entrySet()) {
    try (OutputStream o = Storage.getInstance().createOutputStream(e.getKey())) {
        o.write(e.getValue());
    }
}

The try-with-resources is deliberate: Util.cleanup swallows a close failure, and on the write side a swallowed close means an entry was never finalized under the new key. Let it throw.

This isn’t crash-safe, and it can’t be made crash-safe from the outside. If the process dies part way through the second loop, some entries are under each key and neither opens the whole store. Storage has no rename, so there is no way to stage the converted copies and swap them in atomically, and the recovery data lives only in that in-memory map.

Which is the real argument for not rotating the storage key at all. Encrypt Storage with a random key your app generates once, keep that key wrapped under the password-derived one in a single record outside Storage, and a password change re-wraps that one small record instead of rewriting every entry. Nothing else moves, so there is no half-converted state to recover from. If you do rewrite the store in place, do it once at startup with nothing else touching Storage, and treat the possibility of an interrupted run as something your app has to survive rather than something this loop prevents.

It isn’t a good idea to replace storage objects when an app is running so this is purely for this special case…​

If you use preferences it might be a good idea to set their built-in location to a different path using something like Preferences.setPreferencesLocation("EncryptedPreferences");.

This is useful as it prevents the encrypted preferences from colliding with the regular preferences.

65.3. Secure storage and secrets

Some values are too sensitive for Storage or Preferences, which are plaintext on disk: auth tokens, refresh tokens, and API keys. Codename One gives you two complementary tools for them.

65.3.1. SecureStorage (the platform keychain)

com.codename1.security.SecureStorage keeps small string values in the operating system’s secure keychain (iOS Keychain Services and the Android Keystore) so they’re protected by the platform’s secure enclave instead of being written to app storage. The quiet, no-prompt API is three calls:

SecureStorage ss = SecureStorage.getInstance();
ss.set("auth.token", token);
String token = ss.get("auth.token");   // null if absent
ss.remove("auth.token");

What backs it differs by port, and a null read isn’t the only thing to watch for. The native ports mostly put the value behind something real: Keychain Services on iOS, an AndroidKeyStore key on Android from API 23, DPAPI on Windows and the Secret Service on Linux. Below API 23 the Android path falls back to Base64 in SharedPreferences and warns about itself, so it’s a store and not a protection. The base implementation returns false / null where there’s nothing to use, so treat that as "not available" and fall back accordingly. The browser port now encrypts what it writes: the value goes into origin-private storage as AES-GCM ciphertext under a CryptoKey created with extractable: false and held in IndexedDB, which is a key the page can use and can’t read out. That protects a copied storage file, a profile backup and a stolen database, and it doesn’t protect the value from script running on the origin, which calls get exactly as your code does. Outside a secure context there is no Web Crypto at all, so writing over plain http: is refused rather than stored in the clear. The simulator’s non-prompting tier still describes itself as obfuscation rather than security. Don’t let a successful read in either of those stand in for a test on a device.

65.3.2. Secrets (fetched from the cloud, never shipped in the binary)

Baking an API key (for example a Google Maps key) into your app means it’s in the binary forever, and rotating it means shipping a new build. com.codename1.security.Secrets keeps the value out of the binary and lets you rotate it server-side. Be clear about what that does and doesn’t buy you: the fetch is authenticated with the build key, which is in the binary, so a determined attacker who extracts it can call the vault the same way your app does. What you gain is rotation without a release, and a vault that decides what a client may ever read — not secrecy against someone reverse engineering the binary. You define the secret once in the Codename One Cloud vault, and the app fetches it at runtime and caches it in SecureStorage:

// off the EDT -- the first call may hit the network:
String mapsKey = Secrets.get("googlemaps.key");

get returns the keychain-cached value when present, otherwise it fetches from the vault over TLS, caches it, and returns it. refresh(name) forces a fresh fetch (after you rotate the value server-side) and clear(name) drops the cached copy.

To publish a secret, open the Secrets page of the build console and add it under the app-readable namespace (the app. prefix; you reference it by its bare name from Secrets.get). Only secrets in that namespace are ever served to a device, so your server-only credentials (such as the store API keys used by commerce validation) stay in the vault and can’t be fetched from a client. Authentication uses the build key every cloud build already carries, so there’s nothing to wire up; on the simulator or a local build there’s no build key, so get returns the cached value if any and otherwise null.

Build hintDefaultDescription

secrets.cloud.endpoint

(Codename One cloud)

Base URL of the secrets service. Override only if you self-host.

65.4. Vaults: One password, every device

SecureStorage answers one question — where does this device keep a small secret — and it answers it per device. com.codename1.security.vault.Vault answers the harder one: how does the same user read the same encrypted data on their phone and in a browser, without a server ever being able to read it, and without typing a password on every page load.

One random 32-byte data key protects everything a vault holds. The data key is never stored. What’s stored are wrapped copies of it: one sealed under a key derived from the user’s password, optionally one sealed under a recovery code, and, if the user asked to be remembered, one sealed by this device’s key store — the keychain on iOS, the keystore on Android, a non-extractable CryptoKey in a browser. Unlocking means unwrapping one of those copies. Changing the password rewrites one wrap and re-encrypts no data at all.

That shape is what makes synchronization work. The record describing a vault is ciphertext, and so is every record the vault seals, so a sync server can store both and read neither.

65.4.1. Setting one up

Vault vault = Vault.named("notes");
VaultOptions options = new VaultOptions()
        .policy(rememberThisDevice ? UnlockPolicy.REMEMBER_DEVICE
                : UnlockPolicy.SESSION_ONLY)
        .autoLockAfter(5 * 60 * 1000);
vault.configure(options);
vault.enroll(password, options).ready(ready -> Log.p("vault ready"));

On every later run, try the remembered key first and fall back to asking:

Vault vault = Vault.named("notes");
vault.unlockRemembered()
        .except(notRemembered -> vault.unlockWithPassword(password)
                .except(wrong -> Log.p("that password did not open the vault")));

KEY_MISSING from unlockRemembered is the ordinary case, not an error to show anyone: it’s how you learn this device was never remembered, or has been forgotten.

65.4.2. Storing things

Small secrets go in by name. Values are char[] rather than String so you can clear them — a String is immutable and lives in the heap until it’s collected, which is an unbounded window in a heap dump.

vault.putSecret("api.token", token);
vault.getSecret("api.token").ready(value -> {
    useToken(value);
    // The caller owns the characters and can clear them; a String could not be.
    java.util.Arrays.fill(value, '\0');
});

Larger data is sealed rather than stored, so you decide where it lives:

vault.seal("note-7", note).ready(sealed -> upload("note-7", sealed));
// The vault record itself is safe to upload too: it carries the data key only in
// wrapped form, so a server holding it can neither read the notes nor help anyone else.
upload("vault", vault.exportSyncState());

A second device enrolls from the first device’s exported state and the same password:

Vault vault = Vault.named("notes");
vault.importSyncState(syncState, password)
        .ready(ok -> vault.open("note-7", sealedNote).ready(this::showNote))
        .except(failure -> Log.p("could not enroll this device: " + failure.getMessage()));

Keep the credentials the user signs into your sync server with separate from the vault password. A server that can verify the login must not be able to derive the vault key, and it can if they’re the same string.

65.4.3. Unlock policies

Three, and the choice belongs to the user rather than to you.

PolicyWhat it means

SESSION_ONLY

The password is required every launch, and every page load in a browser. Nothing that can reopen the vault is written to the device.

REMEMBER_DEVICE

The data key is additionally wrapped under this device’s key store, so the vault reopens with no prompt. That’s unattended access: anyone who can run the app on this device reopens the vault without the password.

REQUIRE_USER_VERIFICATION

Reaching the key needs the user to verify themselves. Choosing this deletes the unattended wrap, because a policy that promises a prompt while a wrap that skips it sits alongside promises nothing.

In a browser the third one isn’t the second with a flag on it — it’s a different mechanism. REMEMBER_DEVICE wraps the data key under a non-extractable CryptoKey in IndexedDB; REQUIRE_USER_VERIFICATION wraps it under key material a passkey derives, through the WebAuthn PRF extension.

That distinction is the one thing a browser can offer that the stored key can’t: the key material doesn’t exist until the user verifies. A copied browser profile carries the credential id, which isn’t a secret, and can’t derive anything without the authenticator and a person in front of it. The stored CryptoKey, by contrast, travels with the profile and works in the copy.

A passkey may be synced, and that’s your choice to make. By default the browser offers whatever authenticators it has, so on a Mac the credential can land in iCloud Keychain and reach every device on that Apple account; a Google-account passkey on Android behaves the same way. Where that happens, "remember this device" amounts to "remember this account" — still a genuine protection, because it needs the account and a biometric, but not device-binding.

For a note-taking app that’s usually what the user wants: a new laptop unlocks with a face scan and no password. For something holding a second factor it’s the opposite, which is why this is a choice:

VaultOptions options = new VaultOptions()
        .policy(UnlockPolicy.REQUIRE_USER_VERIFICATION);
if (mustNotLeaveThisDevice) {
    // Excludes security keys and every synced passkey. Enrolment fails with
    // POLICY_NOT_MET where no authenticator can meet it, so have a fallback.
    options.requireDeviceBoundPasskey();
}
Vault vault = Vault.named("payments").configure(options);
vault.enroll(password, options).except(failure -> offerPasswordOnly());

requireDeviceBoundPasskey() checks the credential’s backup-eligibility flag, which is the part that actually decides. Asking for platform attachment alone isn’t enough — an iCloud Keychain passkey is platform-attached and syncs anyway. A browser that won’t report the flag counts as may leave this device, so an unknown answer is never rounded up into a guarantee.

What it costs: hardware security keys are excluded, and a user whose only authenticator syncs can’t enroll at all — enrollment fails with POLICY_NOT_MET rather than keeping a credential that doesn’t meet the requirement. Offer the weaker policy as a fallback, or don’t require it.

Checked on real hardware and not only against a virtual authenticator: macOS Touch ID in Chrome derives 32 bytes, and derives the same 32 bytes again after the browser has been quit and reopened — which is the property the whole scheme rests on, since a passkey that derived fresh bytes each time would make the wrapped data key unrecoverable.

A passkey signature isn’t an encryption key, and it’s worth being clear why, because the obvious shortcut is wrong: signatures are randomized, so signing a fixed challenge gives different bytes every time. PRF is the part of WebAuthn that actually derives a key — the authenticator evaluates its own HMAC secret over a salt, so the same credential and salt give the same 32 bytes forever.

Not every authenticator implements it. Enrollment checks prf.enabled from the creation ceremony and refuses to keep a credential that can’t derive, so capabilities().supports(…​) tells you the truth rather than offering a prompt with nothing behind it. And note that WebAuthn doesn’t work on an IP-address origin at all — a relying-party id has to be a domain. For local development that means serving from localhost, not 127.0.0.1.

65.4.4. Measured browser support

Run by hand against the shipped bridge on macOS, with Touch ID as the platform authenticator:

BrowserDevice keyPasskey PRFstorage.persisted()

Chrome 152

yes

yes

false

Safari 26.6

yes

yes

false

Firefox 155

yes

yes

false

"Device key" is the whole REMEMBER_DEVICE path: a non-extractable CryptoKey structured-cloned into IndexedDB, a wrap round trip, and a wrong binding correctly failing its tag. That’s the one that mattered — it’s the default, and had Safari refused the clone, remembered vaults would have been broken on every iPhone and Mac.

PRF derived 32 stable bytes in all three, and in Chrome the same bytes after the browser was quit and reopened.

What this doesn’t cover: mobile Safari, Chrome on Android, Windows Hello, hardware security keys, and older versions of all three. The runtime detection above is what protects those — the table says what was seen, not what’s guaranteed. Every one of these browsers reported storage.persisted() as false on a fresh profile, which is why PERSISTENT is reported from storage working rather than from that call.

Reproduce any row with node scripts/verify-javascript-vault-passkey.mjs, which serves a page running the same bridge an application does.

REQUIRE_USER_VERIFICATION isn’t available everywhere. Ask vault.capabilities().supports(policy) before you offer the switch, because a "remember this device" toggle on a platform that can’t remember is worse than no toggle.

65.4.5. Asking what actually protects it

Protections are independent flags with three answers each — yes, no, and can’t say — rather than a security level. The third is a real answer: no browser will tell you whether a key ended up in a secure element, and a report that guessed would be inventing a guarantee.

ProtectionReport report = vault.protection();
if (report.answer(Protection.HARDWARE_BACKED) == ProtectionReport.UNKNOWN) {
    // Not "no". This platform will not say, and a screen that rendered that as "no"
    // would understate an authenticator that does use a secure element.
    return "Encrypted on this device; hardware backing could not be confirmed.";
}
return report.provides(Protection.HARDWARE_BACKED)
        ? "Encrypted with a hardware-backed key."
        : "Encrypted with a software key.";

When you need a protection rather than a description of one, require it. The vault then refuses instead of storing with less:

VaultOptions options = new VaultOptions()
        .policy(UnlockPolicy.REMEMBER_DEVICE)
        .require(Protection.OS_PROTECTED);
Vault vault = Vault.named("payments").configure(options);
vault.enroll(password, options).except(failure -> {
    if (failure instanceof VaultException
            && ((VaultException) failure).getError() == VaultError.POLICY_NOT_MET) {
        // The browser reaches this: there is no OS key store in a page. Offer the
        // session-only policy instead of pretending the requirement was met.
        Log.p("this device cannot hold the key in the OS key store");
    }
});

UNKNOWN never satisfies a requirement.

65.4.6. What a vault protects against

ThreatProtected

A stolen database file, a stolen backup, a copied storage pool

Yes. Everything at rest is authenticated ciphertext under a key that isn’t stored beside it.

A copied browser profile, vault locked

Yes under SESSION_ONLY. No under REMEMBER_DEVICE in a browser: the copy contains the IndexedDB the wrapping key lives in, and the key works in the copy.

A copied profile, vault unlocked

No. The data key is in memory.

Script running on your own origin, vault unlocked

No. It calls the same decrypt your code calls. Non-extractable keys and Web Workers don’t change this.

Script on your origin, vault locked

It can’t read the data. It can wait, and read the password when the user next types it.

A browser extension, or malware on the device

No.

A compromised server that serves new application code

No. New code runs in your origin and inherits everything your application has.

Data already copied elsewhere

No. forgetDevice() and destroyLocalData() remove local access; they can’t reach a copy.

The second row is the one that gets misread. "The database file alone is useless" and "a copy of the whole profile is useless" are different claims, and only the first holds for a browser with a remembered device. Say that to users in those words.

65.4.7. Keys you can use and can’t read

operationalKey hands back a KeyHandle — a key with no byte accessor at all. There’s no getEncoded() on it and no method that adds one, and it stops working the moment the vault locks.

vault.operationalKey("cache").ready(key -> {
    // There is no getEncoded() on a KeyHandle, and no method that adds one.
    key.seal(payload, null).ready(sealed -> storeInCache(sealed));
});

65.4.8. Encrypted databases

SQLCipher keys from bytes, on the device and in the browser’s WebAssembly build alike, so a database can’t be keyed by a handle. DatabaseConfig.vault(…​) therefore produces real key bytes, and says so rather than exporting a handle behind your back:

// The vault must be unlocked here: the key does not exist until it is. Locking the
// vault afterwards does not close the connection -- the engine already has the key.
return Database.openOrCreate("notes", DatabaseConfig.vault(vault, "notes"));

While the database is open the key is in the process and stays there; locking the vault doesn’t close the connection. What the vault adds over DatabaseConfig.managed() is that the key doesn’t exist at all until the user unlocks — which in a browser is the difference between a page that holds a database key from the moment it loads and one that doesn’t. If you can’t accept the exposure, call VaultOptions.requireOpaqueKeysOnly(), which makes that call fail with POLICY_NOT_MET and means this vault can’t key a database.

rotateDataKey and importing a rotation both change the current database key. Record vault.getDataKeyVersion() alongside the database when creating it, and use DatabaseConfig.vault(vault, alias, recordedVersion) to open that file even after a later rotation arrives. The vault retains the retired keys across restarts; vault.databaseKey(alias, version) also exposes them for database engines you manage directly.

To change the database key, open the file at its recorded version, call database.changeKey(DatabaseConfig.vault(vault, alias, newVersion)), and update the recorded version only after that succeeds. Keep both version numbers in a local migration journal until reopening succeeds, so a crash between changing the key and updating that record is recoverable. There is no automatic key change.

If import policy setup fails, an untouched import is rolled back. VaultError.IMPORT_COMMITTED means the import reached committed state and rollback would risk existing data or couldn’t be confirmed. The session is locked; reread the persisted sync state before retrying policy setup.

65.4.9. Cross-platform interoperability

Everything about the format is pinned so the same password opens the same data everywhere:

  • AES-256-GCM with a 12-byte nonce and a 128-bit tag, in a versioned envelope whose header — version, cipher suite, KDF parameters, key id — is authenticated rather than merely present. Rewriting the iteration count fails the tag instead of steering the decryption.

  • PBKDF2-HMAC-SHA256, 600,000 iterations by default, with the count carried in the envelope so it can be raised without breaking what’s already written. It’s the one password KDF a browser has, and a KDF one of the three platforms has to emulate in application code isn’t a portable one.

  • UTF-8 encoded by Codename One rather than by the platform, because runtimes disagree about unpaired surrogates. No normalization is applied: if you want to accept two spellings of the same accented password, normalize before you call.

  • Every envelope is bound to its application, vault, record and purpose. Ciphertext moved from one record to another fails to authenticate rather than decrypting into the wrong slot.

65.4.10. Deploying the browser build

The vault needs a secure context. Served over plain http: on anything but localhost there’s no Web Crypto, and writes are refused rather than stored in the clear.

The SQLite engine — about 1.5 MB of WebAssembly and its loader — is copied into the build only when the application can actually reach it. It’s fetched on first use rather than at page load, so it never cost startup time for anyone; it cost deployment size in an artifact you upload to static hosting. The decision is made from the translated output, and it fails toward shipping: if the build can’t tell, the engine goes in, because a database that won’t open is a worse outcome than a download nobody makes.

The JavaScript build writes its own deployment configuration into a cn1-security directory: a _headers file for Netlify and Cloudflare Pages, configuration for nginx and for Apache, and a README explaining them. It’s written beside the bundle directory and named after it — MyApp-js-cn1-security next to MyApp-js — because the bundle directory is the web root you upload. Host configuration doesn’t belong there, and a directory that sits outside the bundle can’t be published by accident. A Maven build copies it, and the suspension report beside it, into target/. Nothing here is active until you copy the file your host reads to where it reads it. That’s on purpose: Netlify and Cloudflare read _headers automatically, and an application that never asked for a policy shouldn’t acquire one on its next deploy. Read the README first: the policy restricts connect-src to this origin, so an app that calls a backend elsewhere has to add it. The Content-Security-Policy in them is generated from the page that was actually emitted — each inline block is hashed — so it doesn’t go stale against a template.

Three of those protections exist only as response headers and are ignored in a <meta> tag: frame-ancestors, X-Frame-Options and Strict-Transport-Security. A deployment that puts the policy in the page and skips the host configuration has no clickjacking protection and no HSTS, whatever the rest of it says.

Give the application its own origin where you can. Storage, IndexedDB, the vault’s key and cookies are all keyed by origin and not by path, so a second application on the same host under a different path shares every one of them.

65.5. Disabling screenshots

One of the common security features some apps expect is the ability to block a screenshot. In the past apps like snapchat required that you touch the screen to view a photo to block the ability to grab a screenshot (on iOS). This no longer works…​

Blocking screenshots is implemented by classifying the app window as secure on Android. You can enable this via the build hint android.disableScreenshots=true. Once that’s added screenshots should no longer work for the app, this might impact other things as well such as the task view which will no longer show the screenshot either.

On iOS, you can’t prevent a user from taking a static screenshot, but you can block screen capture/recording while it’s active. This uses UIScreen.isCaptured and its change notification to hide the app contents while a capture session is active. Enable this behavior with the build hint ios.disableScreenshots=true.

65.6. Blocking copy & paste

Blocking copy & paste is useful for cases where a device might have spyware installed that monitors the clipboard. This also prevents a user from using a password manager (which rely on the clipboard), those managers could be compromised and thus if you’re building a secure app this might be necessary.

You can block copy & paste on Android & iOS. Blocking of copy & paste can be implemented globally or on a specific field.

To block copy & paste globally use:

Display.getInstance().setProperty("blockCopyPaste", "true");

To block copy & paste on a specific field do:

textCmp.putClientProperty("blockCopyPaste", Boolean.TRUE);
Notice that the inverse of using false might not work as expected

65.7. Blocking jailbreak

iOS & Android are walled gardens which is both a blessing and a curse. Looking at the bright side the walled garden aspect of locked down devices means the devices are more secure by nature. For example, on a PC that was compromised you can detect the banking details of a user logging into a bank. But on a phone it would be much harder due to the deep process isolation.

This isn’t true for jailbroken or rooted devices. In these devices security has often been compromised with good intentions (opening up the ecosystem) but it can also be used as a step in a serious attack on an application!

For obvious reasons it’s hard to accurately detect a jailbroken or rooted device but when possible if you have a high security app you might want to block the functionality or even raise a "silent alarm" in such a case. To detect this you can use the isJailbrokenDevice method as such:

if(Display.getInstance().isJailbrokenDevice()) {
    // probably jailbroken or rooted
} else {
   // probably not
}

Notice that this isn’t right, you can’t be 100% sure as there are no official ways to detect jailbreak. That’s why it’s crucial to encrypt everything and assume the device was compromised to begin with when dealing with sensitive data. Still it’s worthwhile to use these APIs to make the life of an attacker a little bit harder.

65.8. Device integrity, RASP & attestation

The isJailbrokenDevice check above is a single, coarse signal. For high-security apps (banking, payments, healthcare) Codename One exposes a richer, cross-platform device-integrity API in com.codename1.security.DeviceIntegrity. It groups three families of runtime self-protection (RASP) primitives, each available both as a zero-code build hint (an automatic launch-time guard) and as a runtime API (so your app can make granular, per-transaction decisions). The build hints are wired into the build server, so enabling a feature is just a settings entry away.

Every on-device check is bypassable on a sufficiently compromised device — that’s an industry truth, not a Codename One limitation. Use the on-device checks for defense-in-depth and UX (warn, degrade, require step-up auth), but gate genuinely sensitive server actions on the server-verified attestation token described below, never on a boolean computed on the device itself.
The device integrity trust boundary

65.8.1. Detecting rooted / jailbroken / tampered devices (RASP)

Codename One ships several launch-time hard gates that terminate the app on a compromised device. They’re enabled through build hints and need no code:

  • android.rootCheck=true — bundles the RootBeer library and exits at launch on a rooted device (su binaries, test-keys / custom ROM signals, Magisk indicators, dangerous system properties).

  • android.fridaDetection=true — detects the Frida dynamic-instrumentation toolkit (the core RASP threat) and exits.

  • ios.detectJailbreak=true — exits at launch on a jailbroken iOS device (MobileSubstrate / Liberty / FridaGadget dylibs, Cydia/apt paths, sandbox-escape probes).

When you’d rather respond in-app than be hard-killed — for example to block only a single risky operation, raise a silent alarm, or require additional authentication — use the non-exiting runtime report instead:

if (DeviceIntegrity.isDeviceCompromised()) {
    // root / jailbreak / Frida / emulator detected -- do not hard-exit,
    // instead degrade: disable the transfer button, require step-up auth, etc.
    for (String reason : DeviceIntegrity.getCompromiseReasons()) {
        // reason is a machine-readable code: "root", "frida", "emulator", "jailbreak"
        Log.p("Device compromise signal: " + reason);
    }
}

isDeviceCompromised() covers the root, jailbreak and Frida ground the launch gates do, plus emulator heuristics, but it isn’t the same set and never terminates the app. On iOS it reports more than the gate enforces: the launch guard exits only on the file, path, mount and hooking signals, leaving out the one that fires when a debugger is attached, by design, so a device launched from Xcode still runs, while the runtime report includes it and also probes for the package managers a jailbreak leaves behind. Don’t read a passed launch as covering whatever your runtime policy checks. On unsupported ports it returns false. The simulator returns false too until you set a signal under Simulate > App Shield, which is how you exercise your warning, degradation and step-up branches without a compromised device.

If your security policy mandates a specific commercial Mobile Threat Defense (MTD) / RASP vendor (Zimperium, Promon SHIELD, etc.), you integrate that vendor’s SDK through the standard android.gradleDep / ios.pods build hints — Codename One’s built-in checks complement, but don’t claim to replace, a dedicated MTD product.

65.8.2. Platform attestation: Google Play Integrity & Apple App Attest

The trustworthy way to verify device and app integrity is hardware-backed attestation: Google Play Integrity on Android and Apple App Attest on iOS. Both produce a signed token that your backend verifies with Google / Apple. This is the mechanism to use when you must, for example, block an outward transfer to a newly added beneficiary on a device that fails integrity.

Enable it with a build hint, which bundles the SDK (Android) or links the entitlement (iOS):

  • android.playIntegrity=true — bundles the Play Integrity SDK and enables the runtime token API. Optionally add android.playIntegrity.verifyUrl=https://backend.example.com/integrity to also attest at launch on a background thread and exit if your backend rejects the token. android.playIntegrityVersion overrides the bundled SDK version.

  • ios.appAttest=true — enables App Attest (DeviceCheck.framework) and the runtime token API. ios.appAttest.environment is development (debug builds) or production (release) by default.

The runtime API is identical on both platforms — request a token bound to a fresh server nonce, then POST it to your backend for verification:

// serverNonce is generated by, and round-tripped from, your backend so it can
// detect replay. The returned token is opaque and MUST be verified server-side.
DeviceIntegrity.requestIntegrityToken(serverNonce).onResult((token, err) -> {
    if (err != null) {
        // Attestation unavailable -- treat the device as untrusted /
        // require an additional verification step before allowing the transfer.
        return;
    }
    // POST `token` to your backend. The backend decrypts/validates the verdict
    // with Google / Apple and decides whether to authorize the transfer.
    authorizeTransferAfterServerVerification(token);
});

DeviceIntegrity.isAttestationSupported() reports whether attestation is available and was bundled into this build. On the simulator, on unsupported ports, or when the build hint is off, requestIntegrityToken() completes with an error rather than throwing.

The block-the-transfer decision lives in your backend, gated on the verified verdict. Codename One’s job is to reliably deliver the attested token; the policy enforcement — for example blocking new-beneficiary transfers when integrity fails and biometric controls aren’t enabled — is application + server logic that consumes this API.

DeviceIntegrity hands you the raw platform blob and leaves verification, policy, replay protection, key management and rotation to you. If you’d rather not build that, Chapter 66, App Shield is the managed layer on top of exactly this API: the attestation is verified server-side against Apple and Google, evaluated against a policy you control, and turned into a short-lived signed token your backend checks with a few lines of middleware. Both remain available and this API keeps working unchanged.

65.8.3. Defending against accessibility-service abuse (Android)

A common Android malware technique is to abuse the accessibility services API for overlays, remote control, and on-screen text extraction (including reading text destined for text-to-speech). Codename One lets you detect untrusted accessibility services and lock down sensitive screens.

The android.accessibilityGuard=true build hint checks the enabled accessibility services at launch and, by default, exits when an untrusted one is active. Tune it with:

  • android.accessibilityGuard.allow=com.google.android.marvin.talkback,com.example.trusted — a comma-separated allow-list of accessibility-service packages the user legitimately relies on (such as a screen reader). Any enabled service outside this list trips the guard.

  • android.accessibilityGuard.mode=exit|warn — exit (default) terminates the app; warn only logs, leaving the decision to your runtime code.

At runtime you get the same information for granular control:

// Enumerate the enabled accessibility services (component ids "pkg/.Service").
String[] services = DeviceIntegrity.getEnabledAccessibilityServices();

// True if any enabled service is outside your trusted allow-list.
if (DeviceIntegrity.hasUntrustedAccessibilityService("com.google.android.marvin.talkback")) {
    // Likely accessibility-abusing malware -- block the sensitive action.
}

For the screens that actually matter (PIN entry, transfer confirmation), mark them secure so the OS blocks screenshots, screen recording, and accessibility screen scraping while they’re displayed:

// Entering a sensitive screen (Android FLAG_SECURE; no-op on iOS):
DeviceIntegrity.setSecureScreen(true);
// null leaving it:
DeviceIntegrity.setSecureScreen(false);
Accessibility-service enumeration and setSecureScreen() are Android concepts; on iOS they’re no-ops (iOS has no equivalent third-party accessibility-service threat model). Use ios.disableScreenshots=true to block screen capture on iOS as described in Section 65.5, “Disabling screenshots”.

65.8.4. Defending against tapjacking and screen overlays (Android)

Tapjacking is the other half of the same attack. Instead of driving your app through an accessibility service, a malicious app draws its own window on top of yours — a fake PIN pad, a fake confirmation button, or an invisible layer — so the user believes they’re tapping your UI while the tap is being framed, or harvested, by someone else. It’s the classic way a banking transfer gets confirmed by a user who thought they were dismissing a notice.

Android exposes this as flags on each touch event, and Codename One turns that into a policy you set once:

  • android.tapjackingGuard=true — switches on protection at launch with no code. By default it blocks obscured touches and asks Android 12+ to hide overlay windows.

  • android.tapjackingGuard.mode=block|strict|report — block (default) drops any gesture that starts while another window covers the touched point. report observes without changing behavior, which is the safe way to measure how often this happens in your user base first. strict also drops touches where only part of the window is covered; read the warning below before choosing it.

  • android.tapjackingGuard.hideOverlays=true|false — whether to also call setHideOverlayWindows(). Defaults to true.

The runtime API gives you the same control plus a notification:

// Switch on tapjacking protection once, during startup. BLOCK drops any gesture
// that begins while another app's window covers the touched point.
DeviceIntegrity.setTapjackingProtection(TapjackingPolicy.BLOCK);

// A blocked gesture never reaches your components, so this listener is how the
// app learns the tap happened. It fires when the state changes, not per touch.
//
// Read the state off the event, not from isScreenObscured(). The callback is
// delivered on the EDT while the state is updated on the platform's input thread,
// so a busy EDT can let a later clean touch land first -- and re-reading the
// global state would then skip the warning for the event that caused it.
DeviceIntegrity.addTapjackingListener(e -> {
    if (Boolean.TRUE.equals(e.getSource())) {
        Dialog.show("Security warning",
                "Another app is drawing over this screen. Close it before continuing.",
                "OK", null);
    }
});

// On a sensitive screen, remove the overlay instead of merely filtering its taps.
// This is Android 12+ only, and unlike the touch filter it also protects native
// peer components such as BrowserComponent.
if (DeviceIntegrity.isHideOverlayWindowsSupported()) {
    DeviceIntegrity.setHideOverlayWindows(true);
}
DeviceIntegrity.setSecureScreen(true);

Two properties of this API are worth being precise about, because getting them wrong leads to a false sense of coverage.

Detection is touch-driven, not a live window query. Android reports obscuring as a flag on a delivered MotionEvent, so isScreenObscured() means "the most recently observed touch arrived obscured." If an overlay appears and the user never touches the screen, nothing is observed. That’s a platform limitation, not an implementation gap.

A blocked gesture is dropped in full. Swallowing only the press and delivering the release would leave the framework holding half a gesture, so once a gesture starts obscured, everything through to the matching release is discarded. Your components see nothing at all — which is exactly why the listener exists, and why an app that blocks should register one rather than rely on polling.

setHideOverlayWindows(true) is the strongest of the three defenses and the only one that also covers native peer components such as BrowserComponent and native text fields, because it removes the offending window instead of filtering the touches it enables. Pair it with setSecureScreen(true) when you enter a sensitive screen and clear both on the way out.

It has two requirements, and isHideOverlayWindowsSupported() reports on both — it returns false rather than claiming a protection you aren’t getting:

  • Android 12 (API 31) or newer.

  • The android.permission.HIDE_OVERLAY_WINDOWS manifest permission. Without it the OS throws, so the call is skipped and logged instead. android.tapjackingGuard=true declares it for you; if you drive the runtime API without that hint, add android.hideOverlayWindows=true. It’s a normal install-time permission — no runtime prompt and no special-access screen for the user.

strict mode blocks touches where only part of the window is covered, and ordinary system UI causes that as a matter of course. On some devices this will discard taps the user intended and the app will simply feel broken, with nothing in the logs to explain it. Use it only where a missed tap is clearly preferable to a hijacked one, and test it on real hardware.
This family is Android-only. iOS gives no application a way to draw over another app, so there is nothing to detect: isScreenObscured() is always false and the policy has no effect. Screen recording and mirroring are a different threat, covered by ios.disableScreenshots=true. In the simulator, Simulate > App Shield > Screen Overlay (Tapjacking) fakes an overlay so you can exercise your listener on the desktop.

65.9. Strong Android certificates

When Android launched RSA1024 with SHA1 was considered strong enough for the foreseeable future, this hasn’t changed completely but the recommendation today is to use stronger ciphers for signing & encrypting as those can be compromised.

APKs are signed as part of the build process when you upload an app to the Google Play Store. This process seems redundant as you generate the signature/certificate ourselves (unlike Apple which generates it for you). But, this is a crucial step as it allows the device to verify upgrades and make sure a new update is from the same original author!

This means that if a hacker takes over your account on Google Play, he still won’t be able to ship fake updates to your apps without your certificate. That’s important since if a hacker would have access to your certificate he could create an app update that would send him all the users private information for example, if you’re a bank this could be a disaster.

Android launched with RSA1024/SHA1 as the signing certificates. This was good enough at the time and is still pretty secure. But, these algorithms are eroding and it’s conceivable that within the 10-15 year lifetime of an app they might be compromised using powerful hardware. That’s why Google introduced support for stronger cryptographic signing in newer versions of Android and you can use that.

65.9.1. The bad news

A downside exists…​

Google introduced that capability in Android 4.3 so using these new keys will break compatibility with older devices. If you’re building a highly secure app this is probably a tradeoff you should accept. If not this might not be worth it for some theoretical benefit.

Furthermore, if your app is already shipping you’re out of luck. Due to the obvious security implications once you shipped an app the certificate is final. Google doesn’t provide a way to update the certificate of a shipping app. Thus this feature applies to apps that aren’t yet in the play store.

65.9.2. The good

If you’re building a new app this is pretty easy to integrate and requires no changes on your part. Just a new certificate. You can generate the new secure key using instructions in articles like this one.

If you’re using Codename One Setting you can check the box to generate an SHA512 key which will harden the security for the APK.

65.10. Certificate pinning

When you connect to HTTPS servers your networking code checks the certificate on the server. If the certificate was issued by a trusted certificate authority then the connection goes through, otherwise it fails. Imagine you’re sitting in a coffee shop connected to the local Wi-Fi, and you try to connect to Gmail to check your email. Since you use HTTPS to Google you trust the connection is secure.

What if the coffee shop was hacked and the router is listening in on everything?

HTTPS is encrypted and the way encryption works is through the certificate. The server sends you a certificate and you can use that to send encrypted data to it.

What if the router grabs the server’s certificate and communicates with Google on your behalf?

This won’t work since the data you send to the server is encrypted with the certificate from the server.

What if the router sends its own "fake certificate"?

That won’t work either. All certificates are signed by a "certificate authority" indicating that a google.com certificate is valid.

What if an attacker could get a fake certificate authorized by a real certificate authority?

That’s a problem!

It’s obviously hard to do but if someone was able to do this he could execute a "man in the middle" attack as described above. People were able to fool certificate authorities in the past and gain fake certificates using various methods so this is possible and probably doable for any government level attacker.

65.10.1. Certificate pinning

This is the attack certificate pinning (or SSL pinning) aims to prevent. You code into your app the "fingerprint" of the certificate that’s "good" and thus prevent the app from working when the certificate is changed. This might break the app if you replace the certificate at some point but that might be reasonable in such a case.

To do this you have a cn1lib. It fetches the certificate fingerprint from the server so you can check the fingerprint against a list of authorized keys. Run mvn cn1:settings, open Extensions, and install SSLCertificateFingerprint. Then use something like this to verify your server:

Fingerprints coded into the app are the part that hurts: the certificate has to be replaced eventually, and a hardcoded pin turns that into an app store release with every un-updated install broken until it lands. Chapter 66, App Shield pins the public key rather than the whole certificate, delivers the pin set over the air with the attestation token, and refuses to publish a set that would brick the app — which is the same protection without the release cycle.

Notice that once connection is established you don’t need to verify again for the current application run.

65.11. Cryptographic primitives

The com.codename1.security package gives you the cryptographic building blocks you need for typical mobile-app concerns — hashing, message authentication, symmetric and asymmetric encryption, digital signatures, JSON Web Tokens, and time/counter-based one-time passwords. The supporting com.codename1.components.OtpField component covers the UI side of OTP entry.

The hash and HMAC primitives are implemented in portable Java so they produce identical output on every platform without depending on any external library. Symmetric and asymmetric encryption, signing, and the secure RNG go through each platform’s native crypto provider via Codename One’s implementation layer:

  • The simulator (JavaSE) and Android route through the JRE’s java.security and javax.crypto.

  • iOS routes through Apple’s CommonCrypto and the Security framework (via Ports/iOSPort/nativeSources/CN1Crypto.{h,m} and the corresponding IOSImplementation overrides).

Application code never sees these differences — you call the same com.codename1.security classes regardless of the platform you are running on.

Cryptographic operations only throw com.codename1.security.CryptoException, an unchecked exception. You can let it bubble up or catch it; you don’t have to declare it.

65.11.1. Hashing

com.codename1.security.Hash implements MD5, SHA-1, SHA-224, SHA-256, SHA-384, and SHA-512. SHA-256 is the recommended default for new code; MD5 and SHA-1 are exposed only for compatibility with older protocols since both are broken for collision resistance:

// One-shot
byte[] digest = Hash.sha256("hello".getBytes("UTF-8"));
String hex    = Hash.toHex(digest);          // 64-character lowercase hex
byte[] back   = Hash.fromHex(hex);           // round-trip helper

// Streaming
Hash h = Hash.create(Hash.SHA256);
h.update(part1);
h.update(part2);
byte[] full = h.digest();                    // hash is reset after digest()

The algorithm identifier strings (Hash.MD5, Hash.SHA256, etc.) are also accepted with or without dashes and in any case ("sha256" and "SHA-256" are equivalent).

65.11.2. Message authentication (HMAC)

com.codename1.security.Hmac implements HMAC (RFC 2104) on top of any of the hash algorithms above. Use HMAC whenever a message authentication code is needed — API request signing, session cookie tamper detection, JWT signing with the HS family, or TOTP token generation:

byte[] tag = Hmac.sha256(secret, message);

// Streaming
Hmac h = Hmac.create(Hash.SHA256, secret);
h.update(part1);
h.update(part2);
byte[] tag2 = h.doFinal();

When comparing authentication tags, use Hmac.constantTimeEquals(a, b) instead of Arrays.equals(a, b) — the latter short-circuits on the first mismatch and is vulnerable to timing attacks.

65.11.3. Secure random numbers

com.codename1.security.SecureRandom exposes the platform’s cryptographically secure RNG (SecRandomCopyBytes on iOS, /dev/urandom on Linux/Android, etc.). Use it for keys, nonces, salts, password reset tokens, and any other security-sensitive value. Never use java.util.Random or Math.random() for these purposes:

byte[] iv  = SecureRandom.bytes(12);      // 12-byte AES-GCM nonce
int    pin = SecureRandom.intBelow(1_000_000);   // bias-free 6-digit code
long   id  = SecureRandom.longBelow(1L << 53);

65.11.4. Symmetric encryption (AES)

com.codename1.security.Cipher exposes AES through the platform’s native AES implementation. AES-GCM is the recommended default because it’s authenticated — a single tag-mismatch failure detects any tampering of the ciphertext or the associated data:

SecretKey key   = KeyGenerator.aes(256);
byte[]    nonce = SecureRandom.bytes(12);
byte[]    aad   = "v1".getBytes("UTF-8");

byte[] ciphertext = Cipher.aesEncrypt(Cipher.AES_GCM, key, nonce, aad, plaintext);
byte[] decrypted  = Cipher.aesDecrypt(Cipher.AES_GCM, key, nonce, aad, ciphertext);

The Cipher class exposes the standard JCE-style transformation strings as constants: Cipher.AES_GCM, Cipher.AES_CBC_PKCS5, Cipher.AES_CBC, and Cipher.AES_ECB_PKCS5. For GCM, the 16-byte authentication tag is appended to the ciphertext (matching the JCE convention). ECB is exposed only for interop with legacy systems — it leaks structure and should never be used for new designs.

65.11.5. Asymmetric encryption and digital signatures (RSA, ECDSA)

com.codename1.security.Cipher also covers RSA encryption, and com.codename1.security.Signature covers digital signatures (RSA and ECDSA). Keys are represented by com.codename1.security.PublicKey and com.codename1.security.PrivateKey, which wrap X.509 SubjectPublicKeyInfo and PKCS#8 DER blobs respectively — the same encodings used by OpenSSL:

// Generate a fresh RSA-2048 key pair (run on a background thread; key
// generation can take a noticeable amount of time)
KeyPair kp = KeyGenerator.rsa(2048);

// Encrypt a small payload (e.g. wrap an AES key)
byte[] sealed = Cipher.rsaEncrypt(Cipher.RSA_OAEP_SHA256, kp.getPublicKey(), payload);
byte[] opened = Cipher.rsaDecrypt(Cipher.RSA_OAEP_SHA256, kp.getPrivateKey(), sealed);

// Sign / verify
byte[] sig = Signature.sign(Signature.SHA256_WITH_RSA, kp.getPrivateKey(), data);
boolean ok = Signature.verify(Signature.SHA256_WITH_RSA, kp.getPublicKey(), data, sig);

To use a key that was generated outside the app, feed the DER bytes to the static factory methods:

PublicKey  pub  = PublicKey.rsa(x509SpkiBytes);          // openssl rsa -pubout -outform DER
PrivateKey priv = PrivateKey.rsa(pkcs8DerBytes);         // openssl pkcs8 -topk8 -nocrypt -outform DER

A key that arrives as a file is usually PEM rather than raw DER — base64 between -----BEGIN----- lines — so PublicKey.fromPem and PrivateKey.fromPem take that text, or the bytes of the file straight from Util.readInputStream, and work out the algorithm from the key itself.

fromPem also reads the containers that aren’t the one the platform wants: the older PKCS#1 RSA PRIVATE KEY and SEC1 EC PRIVATE KEY blocks, and the -----BEGIN CERTIFICATE----- file a server or payment gateway is more likely to hand out than a bare key, whose subject public key it uses. It doesn’t verify the certificate — neither its signature nor its dates — it only reads the key out of it, so a certificate you didn’t already trust is no more trustworthy for having been parsed here.

The Signature class exposes the JCE algorithm strings as constants: SHA256_WITH_RSA, SHA384_WITH_RSA, SHA512_WITH_RSA, SHA256_WITH_ECDSA, SHA384_WITH_ECDSA, and SHA512_WITH_ECDSA.

65.11.6. JSON Web Tokens

com.codename1.security.Jwt provides JWT (RFC 7519) signing and verification. The HS family (HMAC-SHA-2) is pure Java so it works on every platform; the RS family (RSA-PKCS#1 v1.5 with SHA-2) and the ES family (ECDSA with SHA-2) route through the platform’s native crypto:

Map<String, Object> claims = new LinkedHashMap<String, Object>();
claims.put("sub", "user-123");
claims.put("exp", System.currentTimeMillis() / 1000 + 3600);

// Sign with a shared secret (HS256)
String token = Jwt.signHs256(claims, "secret".getBytes("UTF-8"));

// Parse + verify
Jwt parsed = Jwt.parse(token);
if (!parsed.verifyHs256("secret".getBytes("UTF-8"))) {
    throw new SecurityException("bad signature");
}
String subject = (String) parsed.getClaim("sub");

For RSA-signed tokens (RS256/384/512) and ECDSA-signed tokens (ES256/384/512), pass the key to the dynamic overloads:

KeyPair kp   = KeyGenerator.rsa(2048);
String  rs   = Jwt.sign(claims, kp.getPrivateKey(), Jwt.RS256);
Jwt     read = Jwt.parse(rs);
boolean ok   = read.verify(kp.getPublicKey());
Jwt.parse(token) doesn’t verify the signature — you must call one of the verify* methods afterwards. Tokens with an alg header of none are rejected by verify unless you explicitly call setVerifyAllowNoneAlgorithm(true) on the parsed JWT. Accepting unsigned tokens is a critical security bug in almost every deployment, so leave it off unless you have decided that the transport is trusted.

65.11.7. HOTP / TOTP one-time passwords

com.codename1.security.Otp implements counter-based HOTP (RFC 4226) and time-based TOTP (RFC 6238). The output is identical to standard authenticator apps (Google Authenticator, Microsoft Authenticator, 1Password, Authy, etc.) — you can use this class to generate codes inside your app, to validate codes sent by an authenticator on a sign-in flow, or for SMS-based two-factor flows.

Shared secrets are commonly distributed as Base32 strings (the format embedded in QR codes by authenticator apps). com.codename1.security.Base32 handles the encoding:

byte[] secret = Base32.decode("JBSWY3DPEHPK3PXP");

// Generate the current 6-digit TOTP code (30-second step, SHA-1)
String code = Otp.totp(secret);

// Verify the code the user typed in, allowing one step of clock skew on
// either side (so the previous, current, and next codes are all accepted)
boolean ok = Otp.verifyTotp(secret, userInput, 1);

For HOTP, supply the counter explicitly and increment it after every successful authentication:

String code = Otp.hotp(secret, counter, 6);
// advance only once the verifier has accepted it, or a lost code
// desynchronizes this client from a verifier with no look-ahead window
if (serverAccepted(code)) {
    counter++;
}

65.11.8. Walkthrough: Adding two-factor authentication to a sign-in flow

The typical TOTP-based two-factor authentication flow has two distinct phases. The first is enrolment, where the server provisions a shared secret to the user’s authenticator app; the second is verification, which happens on every subsequent sign-in.

The TOTP secret should be generated and stored on your server, not on the Codename One client. The client only ever sees the secret long enough to display the enrolment QR code, then forgets it. Verification on every subsequent sign-in is also a server-side concern (the client just sends the code the user types in). The code samples below put the cryptography on the client only to keep the example self-contained — in production, the server does the actual generation and verification.

Phase 1 — Enrollment. When the user opts into 2FA, generate a 20-byte random secret and show it to their authenticator app as a QR code that encodes a standard otpauth://totp/…​ URI:

// 1. Generate (or fetch from your server) a 20-byte secret -- 160 bits is the
//    de-facto standard for authenticator compatibility.
byte[] secret = SecureRandom.bytes(20);

// 2. Build the otpauth URI. The authenticator app stores the secret against
//    the "Acme Bank: [email protected]" label and tags it with the issuer
//    so it can show "Acme Bank" next to the rotating code.
String uri = Otp.otpauthUri("Acme Bank", "[email protected]", secret);

// 3. Render `uri` as a QR code and show it on screen for the user to scan.
//    See "Rendering the QR code" below.

The maximum-compatibility settings — 6 digits, 30 second step, SHA-1 — are the defaults of Otp.otpauthUri(issuer, accountName, secret). Pass digits / step / algorithm explicitly via the full overload only if you have a specific reason; some authenticator apps don’t recognize anything except the defaults.

Rendering the QR code. Codename One core doesn’t currently ship a QR-code generator. Pick whichever option fits your deployment:

  • Server-side render (simplest). Send uri to your backend over HTTPS and have it return a PNG. The Google Charts API endpoint https://chart.googleapis.com/chart?cht=qr&chs=300x300&chl={url-encoded-uri} and the open-source https://api.qrserver.com/v1/create-qr-code/?data={url-encoded-uri} both work; the latter doesn’t require Google API setup. Render the resulting PNG with URLImage or EncodedImage.create(bytes).

  • Use a QR cn1lib. Community cn1libs such as QRMaker provide native generation. Add as a normal cn1lib dependency.

  • Display the secret as a typed value. If you can’t render a QR code, show the user the Base32-encoded secret (the value after secret= in uri) and ask them to type it manually into their authenticator’s "set up by hand" flow. Less convenient but works on every platform.

Phase 2 — Verification. On every sign-in after enrolment, ask the user for the current six-digit code and verify it against the stored secret. Use [com.codename1.components.OtpField] for the input:

OtpField field = new OtpField(6);
field.addCompleteListener(new ActionListener() {
    public void actionPerformed(ActionEvent evt) {
        String userInput = field.getText();
        // tolerance=1 allows the previous, current, and next 30-second
        // windows to compensate for clock skew between the device and the
        // server. tolerance=0 is strict but unforgiving; tolerance=2 or 3
        // is more permissive but increases the brute-force attack surface.
        if (Otp.verifyTotp(secret, userInput, 1)) {
            // Where this check runs on your server, record the time
            // step this code belongs to and reject a second code from
            // the same step: verifyTotp is stateless, so a captured
            // code stays valid for the rest of the tolerance window.
            grantAccess();
        } else {
            field.clear();
            Dialog.show("Invalid code", "Try again", "OK", null);
        }
    }
});
form.add(field);

Server-side verification. When your backend does the verification, the corresponding Java looks the same — Otp.verifyTotp(storedSecret, codeFromClient, 1). Make sure you rate-limit verification attempts (for example, lock the account after 5 wrong codes within a minute) and log failures, since the 6-digit search space is small enough to be brute-forced without that. Record the time step that each accepted code belongs to and refuse a second code from the same step for that user. verifyTotp is stateless, so a code stays valid for the rest of the tolerance window once it’s been used, and rate limiting doesn’t help because a replayed code isn’t a wrong one. RFC 6238 requires this single-use check.

65.11.9. OTP input widget

com.codename1.components.OtpField is a segmented input — one box per character, that advances to the next box as the user types and steps back on backspace. This is the standard pattern for SMS confirmation and authenticator-app entry screens:

OtpField otp = new OtpField(6);                  // 6 digits, numeric
otp.addCompleteListener(new ActionListener() {
    public void actionPerformed(ActionEvent evt) {
        if (Otp.verifyTotp(secret, otp.getText(), 1)) {
            // accepted
        } else {
            otp.clear();
        }
    }
});
form.add(otp);

Style the boxes through the UIIDs OtpField (the container) and OtpDigit (each box). The component supports alphanumeric input via the two-argument constructor (new OtpField(8, false)), and pasting a full code into any one box distributes the characters across the remaining boxes automatically.

65.11.10. Algorithm identifier reference

The string constants exposed by these classes are listed below so you can pick them out at a glance.

ClassConstantAlgorithm string

Hash

MD5, SHA1, SHA224, SHA256, SHA384, SHA512

MD5, SHA-1, SHA-224, SHA-256, SHA-384, SHA-512

Cipher

AES_GCM

AES/GCM/NoPadding

Cipher

AES_CBC_PKCS5

AES/CBC/PKCS5Padding

Cipher

AES_CBC

AES/CBC/NoPadding

Cipher

AES_ECB_PKCS5

AES/ECB/PKCS5Padding (legacy)

Cipher

RSA_OAEP_SHA256

RSA/ECB/OAEPWithSHA-256AndMGF1Padding

Cipher

RSA_PKCS1

RSA/ECB/PKCS1Padding (legacy)

Signature

SHA256_WITH_RSA, SHA384_WITH_RSA, SHA512_WITH_RSA

SHA256withRSA, SHA384withRSA, SHA512withRSA

Signature

SHA256_WITH_ECDSA, SHA384_WITH_ECDSA, SHA512_WITH_ECDSA

SHA256withECDSA, SHA384withECDSA, SHA512withECDSA

Jwt

HS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, ES512

Same as the JWT alg header value

RSA_OAEP_SHA256 uses SHA-256 for the label hash and for MGF1 alike. The JCE reading of that same transformation name leaves MGF1 on SHA-1, so a JVM backend that names the string and nothing else produces a BadPaddingException against ciphertext from a Codename One app, and the other way round. Codename One can’t follow the JCE here: Web Crypto and Apple’s SecKey both take one hash and apply it to both, so the split pairing can’t be expressed on the JavaScript or iOS ports at all. Have the other side name SHA-256 for the mask as well, which on a JVM means passing an OAEPParameterSpec of ("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, PSource.PSpecified.DEFAULT) to Cipher.init rather than relying on the provider default. Dropping to RSA_PKCS1 also makes the two sides agree, and is the wrong fix: PKCS#1 v1.5 encryption padding is what OAEP exists to replace.
  • AES: 256 bits (KeyGenerator.aes(256)) for new code. 128 bits is also acceptable and faster; pick 192 only if you have a specific reason.

  • RSA: 2048 bits minimum (KeyGenerator.rsa(2048)). Use 3072 or 4096 if you need a longer security margin — key generation gets slow above 2048 so run it on a background thread.

  • HMAC: a key at least as long as the hash output (32 bytes for HMAC-SHA-256). KeyGenerator.hmac(256) returns a key of the requested bit length.

  • TOTP / HOTP: 160 bits (20 bytes) is the de-facto standard for authenticator-app compatibility.

66. App Shield

The security chapter covers a lot of ground that ends at the same place: every check runs on the device, and a device the attacker fully controls is a device where every check can be patched out. Root detection returns false. The pinning code is skipped. The jailbreak probe is stubbed. None of it reaches your server, which is where the money is.

App Shield closes that loop. The app obtains a short-lived, Codename One-signed attestation token backed by a server-side verification of Apple’s App Attest and Google’s Play Integrity statements, and your own API refuses to serve a request that doesn’t carry a valid one. The statement that matters is made by a party the attacker doesn’t control, and your backend can check it in about twenty lines of middleware.

App Shield doesn’t make an app unhackable, and no product does. What it changes is the cost: scripted abuse of your API goes from "curl in an afternoon" to "reverse-engineer a native binary, and start again after every rotation," and your backend gets a cryptographically verifiable statement it can act on. Be careful not to promise more than that, internally or in marketing.

This is an Enterprise feature. The public API in com.codename1.security.shield ships in the open-source core and is safe to call in any build — in a build without the attestation engine it degrades to a documented no-op rather than failing — but tokens are only ever minted for entitled accounts.

66.1. How it fits together

Three parties are involved: the app, the Codename One attestation service, and your own API. A protected request goes through five steps.

  1. The app asks the attestation service for a challenge, and the service answers with a nonce it generated. The nonce is what stops a token captured once from being replayed later.

  2. The app asks the platform to attest itself against that nonce. Apple’s App Attest or Google’s Play Integrity produces a statement signed by hardware the app can’t forge.

  3. The app sends that statement to the attestation service, together with any runtime signals it has collected.

  4. The service verifies the statement with Apple or Google, applies the policy you configured, and answers with a short-lived ES256 JWT and the current certificate pin set.

  5. The app sends that token to your API in the X-CN1-Attest header, and your middleware verifies its signature against the public keys the service publishes.

Steps 1 to 4 are the framework’s job and happen on the network thread. Step 5 is automatic for every ConnectionRequest to a host you registered. The decision — what to do about a device the service declined — is yours, and it belongs in step 5, on your server.

66.2. Turning it on

Two things have to be true: the account has to be entitled, and the build has to ask for it.

Build hintDefaultDescription

shield.enabled

false

Master switch. The build server injects the hardening engine and wires it into the app’s startup. A build that asks for it without an Enterprise subscription, or on a server where the engine is unavailable, fails with an explanation rather than producing an unprotected app and saying nothing.

shield.policyVersion

(service default)

The policy generation this build was compiled against.

shield.pins

(none)

The initial pin set, so the first launch on a new install is pinned before it has ever reached the service. Comma-separated base64 SHA-256 SPKI digests.

shield.jwksPin

(none)

Pins the key that signs the tokens, so a compromised DNS answer can’t introduce a signing key the app will trust.

shield.attestationVerifyUrl

(Codename One cloud)

Override for a private deployment of the verification service.

shield.hardFail

false

The hardening engine’s own failure policy: false reports a problem inside the engine as a signal and lets the app run, true makes it fatal. Leave the default unless support advises otherwise.

ios.appAttest.environment

development for debug builds, production for release

The App Attest environment written into the entitlement.

The engine is injected at build time and isn’t present in a source checkout, in the simulator, or in an unentitled build. That’s deliberate: the detection logic is the part an attacker most wants to read. Everything in this chapter that’s API is open source; only the engine behind it isn’t.

66.3. Protecting hosts

Registration is the whole configuration. A host you don’t register is never touched — no header, no certificate check, no behaviour change of any kind.

AppShield.init(new ShieldConfig()
        // Money moves here, so refuse the request when no token can be obtained.
        .protect("api.mybank.example", HostPolicy.ENFORCED)
        // Everything else under the domain gets a token when one is available.
        .protect("*.mybank.example", HostPolicy.PROTECTED));

// From here on nothing changes at the call sites. A request to a protected
// host is given its token and its certificate check on the network thread.
ConnectionRequest r = new ConnectionRequest("https://api.mybank.example/transfer", true);
NetworkManager.getInstance().addToQueueAndWait(r);

HostPolicy is two independent decisions plus what to do when the shield can’t deliver:

PolicyTokenPinsWhen no token can be obtained

HostPolicy.UNPROTECTED

no

no

Not applicable. This is what an unregistered host gets.

HostPolicy.PROTECTED

yes

yes

Fail open. The request goes out without a token and your backend decides. Right for most hosts.

HostPolicy.ENFORCED

yes

yes

Fail closed. ShieldException is thrown and nothing is sent. Right for the handful of endpoints that move money or read personal data.

Fail-closed is a promise that a request without a valid token never leaves the device, so it applies to the awkward cases too: a protected host reached over plain http is refused rather than handed the bearer token in clear text, and a shield that was interrupted before it finished starting refuses rather than guessing.

If most of your hosts want the same failure mode, set it once and register them the short way:

AppShield.init(new ShieldConfig()
        .defaultFailureMode(FailureMode.CLOSED)
        // Registered the short way, so it picks up the default above.
        .protect("api.mybank.example")
        // And this one opts back out, because an outage here should degrade
        // rather than block the app.
        .protect("images.mybank.example", HostPolicy.PROTECTED));
The default failure mode is read when a host’s policy is resolved, not when it’s registered, so .protect(h).defaultFailureMode(CLOSED) and the same two calls in the other order mean the same thing.

66.3.1. Wildcards and hosts discovered at runtime

A pattern may be an exact host or a leading . wildcard covering its subdomains — .example.com matches api.example.com and a.b.example.com, but not the apex example.com and not example.com.somewhere-else.test. The most specific rule wins, so an exact registration is never relaxed by a wildcard.

Backends the app only learns about after it starts — a regional endpoint chosen at login, a tenant-specific host — are registered the same way, with the same matching:

// Exact host or a "*." wildcard, resolved the way the configured ones are:
// the most specific rule wins, and a runtime registration beats a configured
// one for the same pattern.
AppShield.addProtectedHost(region + ".api.mybank.example", HostPolicy.ENFORCED);
AppShield.addProtectedHost("*.cdn.mybank.example", HostPolicy.PROTECTED);

66.4. What happens to a request

The order the shield works through a request on a protected host

Once init() has run, ConnectionRequest, Rest, RequestBuilder and anything else built on NetworkManager are covered without further code. For a request to a protected host, on the network thread:

  • It waits for initialization to finish if a cold start is still in progress, rather than treating "not ready yet" as "not protected."

  • Any token the shield attached earlier is taken back off before the redirected request is sent — a redirect reuses the same request object, and a bearer token that follows a redirect off a protected host has been handed to whoever the redirect points at. A header your own onRedirect() installed under the same name is left alone: the shield removes its token only while the value is still the token it attached. Note onRedirect() runs only where the framework follows the redirect — iOS redirects inside the native stack and never calls it, so there is no opportunity to substitute a header there.

  • The token is added under the token header, unless the URL isn’t https, in which case the host’s failure mode decides.

  • The certificate chain is collected and checked against the pin set, before any request body is written.

The response side is just as narrow: the guard sees the status code and the headers it asked for, which is how a backend tells the app its token was refused as opposed to the user’s credentials.

66.4.1. The token header

The default is X-CN1-Attest. It isn’t Authorization, by design: that slot belongs to your own user authentication, and the two answer different questions — who the user is, versus whether this is a genuine unmodified app on an uncompromised device. Your backend needs both, so they have to compose.

ShieldConfig.tokenHeader(String) changes it, and refuses names that can’t carry a token rather than accepting them and failing out of sight later:

  • Anything that isn’t a legal HTTP field name. A name with a space or a separator in it isn’t the header you asked for by the time it reaches the server — and it also slips past every check below, because Cookie ` isn’t `Cookie.

  • Content-Type, which ConnectionRequest routes to the request’s media type rather than the header map, so the token would replace the content type and survive the redirect cleanup.

  • Your app’s cookie header, whatever it has been renamed to, because the cookie string is written after the request’s own headers and would overwrite the token after attach() reported success.

  • Headers the transport owns — framing, routing, and the hop-by-hop names a correctly behaving proxy consumes and doesn’t forward.

66.5. Status, and what to do about it

The most important distinction in the API is between "the shield couldn’t be reached" and "the shield looked at this device and said no." Reacting to the first the way you react to the second is how an app locks a paying customer out on a train.

StatusTransientMeaning

OK

-

A valid token is in hand.

UNPROTECTED

no

This build has no attestation engine. Nothing is enforced.

NOT_INITIALIZED

no

init() hasn’t run, or was interrupted.

NO_NETWORK, POOR_NETWORK

yes

The service couldn’t be reached.

SERVICE_DOWN

yes

The service answered with an error.

RATE_LIMITED

yes

This device is being throttled. Retry after a backoff.

REJECTED

no

The service evaluated this device and declined to mint a token.

PIN_MISMATCH

no

A pinned host presented a chain matching none of its pins. Nothing was sent.

ShieldStatus is a constant object rather than an enum on purpose: the vocabulary is visible on the wire and the service must be able to add a status without breaking apps built against an older release. Compare with equals, branch on isSuccess() and isTransient(), and treat an id you don’t recognize as a non-success.

Listeners are delivered on the EDT, so they may touch the UI directly. The framework never shows a dialog and never terminates the app over a shield event — what the user sees is entirely your decision:

AppShield.addListener(new ShieldListener() {
    public void statusChanged(ShieldStatus status) {
        if (status.isSuccess()) {
            return;
        }
        if (status.isTransient()) {
            // The service could not be reached. This is a bad connection, not a
            // bad device -- treating it like a rejection is how an app locks out
            // users on a train.
            Log.p("AppShield: attestation unavailable (" + status.getId() + ")");
            return;
        }
        // REJECTED or PIN_MISMATCH: the service evaluated this device and
        // declined, so degrade rather than retry.
        disableTransfers();
    }

    public void signalRaised(ShieldSignal signal) {
        // Informational. The server decides what a signal means -- an emulator
        // signal is normal on a developer's machine.
        Log.p("AppShield signal " + signal.getId() + " severity "
                + signal.getSeverity());
    }

    public void tokenRefreshed(ShieldToken token) {
    }
});

66.6. Getting a token yourself

For anything the guard doesn’t cover — a third-party HTTP client, a payload you sign yourself, a handshake — ask for the token directly. fetchToken() is asynchronous and returns immediately, including during a cold start:

AsyncResource<ShieldToken> pending = AppShield.fetchToken();
pending.ready(new SuccessCallback<ShieldToken>() {
    public void onSucess(ShieldToken token) {
        sendToMyBackend(token.getValue());
    }
});
pending.except(new SuccessCallback<Throwable>() {
    public void onSucess(Throwable error) {
        ShieldStatus status = ((ShieldException) error).getStatus();
        Log.p("AppShield: no token (" + status.getId() + ")");
    }
});

Pass a binding value to tie the token to one specific request, so a token lifted from it can’t be replayed onto another:

// The digest travels in the token, so a token lifted off this request and
// replayed on another one does not verify against the other one's body.
AppShield.fetchToken(sha256(payload)).ready(new SuccessCallback<ShieldToken>() {
    public void onSucess(ShieldToken token) {
        sendToMyBackend(token.getValue());
    }
});

AppShield.attach(ConnectionRequest) does the same work synchronously for a request you are about to send. It blocks on a network round trip, so it must not be called on the EDT:

ConnectionRequest r = new ConnectionRequest("https://api.mybank.example/transfer", true);
try {
    // Blocks on the attestation round trip, so never on the EDT. In normal use
    // the guard does this for you on the network thread.
    AppShield.attach(r);
} catch (ShieldException err) {
    if (ShieldStatus.PIN_MISMATCH.equals(err.getStatus())) {
        // The chain presented for a pinned host matched no pin. Nothing was
        // sent, and this is the one status worth surfacing to the user: it
        // usually means the connection is being intercepted.
        warnAboutInterception();
        return;
    }
    // A fail-closed host with no token. Retry later if the status is transient.
    Log.e(err);
    return;
}
NetworkManager.getInstance().addToQueueAndWait(r);

Token lifetime is measured from a monotonic clock, never from exp against the device clock, because on a rooted device the wall clock is whatever the attacker wants it to be. getCachedToken() returns the current token without any network access, and invalidateToken() discards it so the next request re-attests — which is what to call when your backend tells you the token itself was refused:

ConnectionRequest r = new ConnectionRequest("https://api.mybank.example/transfer", true) {
    @Override
    protected void readHeaders(Object connection) throws IOException {
        // Your middleware sets this header when the ATTESTATION was the problem,
        // as opposed to the user's own credentials. Without it a 401 or 403 is
        // ambiguous, and an app re-attests its way through a wrong password.
        if (getHeader(connection, AppShield.REJECT_HEADER) != null) {
            AppShield.invalidateToken();
        }
    }
};
NetworkManager.getInstance().addToQueueAndWait(r);
AppShield.REJECT_HEADER is X-CN1-Attest-Reject. Have your middleware set it only when the token was the problem: a protected API normally carries your own user authentication too, and an app that reads every 401 as an attestation failure re-attests its way through a wrong password.

66.7. Certificate pinning

Pins are published by the service and refreshed with the token, so rotating a certificate is a console action rather than an app store release. They’re pinned over the subject public key info, so renewing a certificate on the same key pair doesn’t invalidate the pin, and a chain matches if any certificate in it matches any pin for the host — which is what makes it safe to pin an issuing CA as the backup.

Pinning is the one part of the shield that can take an app offline for a reason the developer can’t fix remotely, so the failure behaviour is asymmetric by design:

ConditionBehaviour

Host has no pins

Never enforced. Covers first run, a cold start with no network, and any host the service hasn’t published pins for.

Pin fetch failed

Never fails a request. The last known set is kept.

Set past its soft expiry

Still enforced; a refresh is attempted.

Set past its hard expiry

Dropped, and enforcement stops. A device offline for weeks loses pinning, not the app.

Chain matches a pin

Request proceeds.

Host has pins, chain matches none

Fail closed. ShieldException with PIN_MISMATCH, before any body is written.

PinSet pins = AppShield.getPinSet();
if (!pins.isEnforcedFor("api.mybank.example")) {
    // No published pins for this host yet, or the set has hard-expired on a
    // long-offline device. Pinning is not enforced, and the app still works.
    Log.p("AppShield: no pins in force (version " + pins.getVersion() + ")");
}
A pin mismatch surfaces as an exception on the request, not as a silent empty response. If you swallow IOException at your call sites you won’t see it, and an intercepted connection is the one failure worth telling the user about.

66.8. Runtime signals

The framework’s own detections — root, jailbreak, hooking frameworks, emulators, debuggers, repackaging, untrusted accessibility services — are reported to ShieldSignals rather than acted on locally, and ride along with the next token fetch. There is no separate beacon to block and no extra battery cost.

This is a deliberate inversion of the older exit(0) style checks. Terminating the app is trivially patched out, destroys the telemetry that makes the feature useful, and turns a detection into a crash report. The client reports; the service decides. An emulator signal is normal on a developer’s machine; ten high-severity signals from one device in an hour is a device the service can keep flagging on its next clean-looking attestation.

Your own code can report what only it can notice — a server-side consistency check that failed, a sequence of actions no human performs:

// Severity is 0-100. The service applies the policy; the client only reports.
ShieldSignals.add("serverStateMismatch", 60,
        "balance disagreed with the ledger after a retry");

// The framework's own detections arrive the same way.
for (ShieldSignal s : AppShield.getSignals()) {
    if (ShieldSignal.HOOK.equals(s.getId())) {
        requireStepUpAuth();
    }
}

The bus is bounded and repeat reports of an id collapse onto the existing entry, so a detector that trips on every frame can’t exhaust memory. An identical repeat refreshes the entry without notifying listeners again; a changed severity or detail is a new observation and is announced.

66.9. Transports the guard doesn’t cover

Coverage matters more here than anywhere else in this chapter, because a gap is invisible at the call site: the code compiles, the request succeeds, and no token was ever attached.

TransportCoverage

ConnectionRequest, Rest, RequestBuilder, NetworkManager

Token and pinning, automatically.

WebSocket

Token via header() on the opening handshake — and only on Android, desktop, Windows and Linux. iOS and the browser hand the handshake to a platform socket that exposes no way to add a header, which drops it without reporting anything. Pinning isn’t reachable through the WebSocket implementations at all.

BrowserComponent

The initial navigation only, through setURL(url, headers). Requests the loaded page makes for itself are invisible to the framework: no token, no pinning.

A native or third-party HTTP client

Nothing automatic. Use fetchToken() and set the header yourself.

// BrowserComponent: the INITIAL navigation only. Requests the loaded page makes
// itself are invisible to the framework and cannot be given a token or pinned.
Hashtable headers = AppShield.headersFor("https://api.mybank.example/statement");
browser.setURL("https://api.mybank.example/statement", headers);

// WebSocket: the handshake carries the token. Emitted on Android, desktop,
// Windows and Linux; silently dropped on iOS and in the browser, whose platform
// sockets expose no way to add a header.
WebSocket.build("wss://api.mybank.example/stream")
        .header("X-CN1-Attest", token)
        .connect();

66.10. Composing with your own network guard

NetworkManager holds exactly one guard and seals the slot after the first one is installed, so an app that needs its own interception has to delegate to the shield’s rather than displace it:

// NetworkManager holds exactly one guard and seals the slot, so an app with its
// own has to delegate rather than replace -- calling attach() by hand instead
// would restore the token and silently drop the certificate callbacks that
// enforce pinning.
final NetworkGuard shield = AppShield.getNetworkGuard();
NetworkManager.setNetworkGuard(new NetworkGuard() {
    public void beforeRequest(ConnectionRequest r) throws IOException {
        r.addRequestHeader("X-My-Trace", newTraceId());
        shield.beforeRequest(r);
    }

    public boolean isCertificateCheckRequired(String url) {
        return shield.isCertificateCheckRequired(url);
    }

    public void checkCertificates(ConnectionRequest r, SSLCertificate[] chain)
            throws IOException {
        shield.checkCertificates(r, chain);
    }

    public String[] interestingResponseHeaders() {
        return shield.interestingResponseHeaders();
    }

    public void afterResponse(ConnectionRequest r, int code, String[] headers) {
        shield.afterResponse(r, code, headers);
    }
});
Don’t "work around" the single slot by dropping the shield’s guard and calling attach() by hand. That restores the token and drops the certificate callbacks with no error, so pinning stops being enforced while everything still looks like it’s working — the half of the shield whose absence looks exactly like success.

66.11. Verifying the token in your backend

The token is a compact ES256 JWT with typ of cn1-attest+jwt, signed by a key published at /.well-known/cn1-attest-jwks.json and named by the header’s kid. Verification is ordinary JWT verification against a JWKS URL — NimbusJwtDecoder.withJwkSetUri in Spring, jose in Node, PyJWT[crypto] in Python — and the console renders a copy-paste quickstart for each, including a Cloudflare Worker for the zero-code-change case.

Beyond the standard iss, aud, sub, iat, exp and jti, the claims your middleware acts on are:

  • pkg and plat — the package name and platform the token was minted for. Check them against the app you expect.

  • vrd — the verdict: attested, monitored or unverified.

  • dec — the decision under the current policy: allow, or would_deny while the app is still in monitor mode.

  • sig — the signals the device reported.

  • pol — the policy version the decision was made under.

  • ph — the payload binding, when the request asked for one.

Roll it out in two stages, and keep the two sides independent. While the service is in monitor mode it mints a token for everyone and marks the ones it would have refused, so you can watch the would-deny rate against real traffic before anything breaks. Your middleware has its own switch: log and pass through, or reject. Move one at a time.

Reject with an opaque 403, not a 401. A 401 invites the client to retry with fresh credentials, which is exactly the loop you don’t want, and a detailed reason is an oracle for whoever is probing you.

66.12. iOS App Attest specifics

Apple’s model is one attestation, then many assertions: the first token carries a hardware-backed public key and its attestation, and every later token is a small assertion naming the key. The framework manages the key, the state and the retry policy in the keychain, including recovery when iOS invalidates the key — a restore to a new device, or a reinstall — and Apple’s rate limits, which are strict enough that attesting per request gets an app throttled.

The one thing your code has to do is close the loop when the backend has stored the key, because a registration call that was lost leaves the device asserting against a key the server has never seen:

// iOS App Attest is attest-once, assert-many: the FIRST token carries the public
// key, every later one is an assertion naming it. Call this once your backend
// has stored that key, or a device whose registration call was lost keeps
// sending assertions for a key the server never saw.
DeviceIntegrity.confirmAttestation(keyId);

DeviceIntegrity.resetAttestation() throws the key away and starts again, which is the recovery path for a backend that no longer knows the key.

App Attest doesn’t work in Apple’s iOS simulator — it needs the Secure Enclave. Test the iOS path on a real device, and use the simulator menu below for everything else.

66.13. Testing it in the simulator

None of this is testable against a real service on a developer’s desktop, so the simulator ships a simulation engine behind Simulate > App Shield. Switching on any item installs it; leaving the menu alone leaves the shield inert.

Menu itemWhat it makes reachable

Attestation Supported

Unchecking it simulates a platform with no attestation at all.

Attestation Result

PASS, or each failure in turn: FAIL_REJECTED, FAIL_NO_NETWORK, FAIL_SERVICE_DOWN, FAIL_RATE_LIMITED, UNSUPPORTED. This is how you exercise the transient-versus-final branch in your listener.

Device signal toggles

Rooted / jailbroken, hooking framework, emulator, debugger, repackaged, untrusted accessibility service — each raising the signal your code reacts to.

Screen Overlay (Tapjacking)

Fakes another app drawing over yours. It drives the real reporting path rather than setting a flag, so your tapjacking listener fires and the tapjack signal is raised exactly as it would be on a device. The desktop has no overlay windows, so this is the only way to reach that branch outside Android.

Serve Expired Token

The refresh path, without waiting out a real token’s lifetime.

Force Pin Mismatch On Next Request

The most valuable item here: a fail-closed pinning branch is otherwise only reachable by mis-pinning a live host on purpose. It’s a one-shot that disarms itself once spent.

Fail Pin Fetch

Confirms that a failed pin refresh doesn’t fail requests.

Show Shield Status…​

What the simulation is currently set to, and whether the simulated engine is registered.

The simulated token is stamped so it can’t be mistaken for a real one, and the simulator’s engine never contacts the service. A backend that accepts a simulated token is a backend that isn’t verifying signatures.

66.14. When there is no engine

Every method in this chapter is safe to call in a build without the engine — an open-source build, an unentitled account, the simulator with the menu untouched. The degradation contract is explicit, and it’s the reason you don’t need to guard your call sites:

  • init() succeeds and logs one line. isProtected() returns false.

  • fetchToken() always completes, with UNPROTECTED, and never hangs.

  • attach() does nothing. No header, no exception.

  • No request is ever blocked, including one to a host registered as ENFORCED. Fail-closed enforcement requires an engine to fail; with no engine there is nothing to enforce and the app must keep working.

  • getSignals() still returns real DeviceIntegrity data, so the free-tier detections described in the security chapter keep working.

67. App Hardening

Every Codename One app is already obfuscated by default. On Android the release build runs through R8, which renames the Java names. On iOS and the other native ports your code is compiled to native machine code through ParparVM, which is hard to reverse engineer on its own — but the class names, method names and string literals still travel into the binary as readable text, so a reader who can’t follow the machine code can still read the labels and the constants.

App Hardening is the Enterprise layer that closes that remaining gap across all the ports from one place. It renames classes, methods and fields; encrypts string constants so they’re not present as plaintext in the binary; and obfuscates control flow — and it does this to the merged application before each platform build, so Android, iOS, JavaScript and the native desktop targets are all covered by one transform and one mapping.

This is a tool for apps that face serious security scrutiny — banking, payments, government and other high-risk targets. It raises the cost of static analysis and tampering; it’s one layer, so pair it with Chapter 66, App Shield so the statement your backend trusts is made by hardware the attacker doesn’t control.

App Hardening is an Enterprise feature. A build that asks for it without an Enterprise subscription fails with an explanation rather than producing an unhardened binary that would look protected without being so.

67.1. What it changes, per port

The transform runs on the merged application jar, at the bytecode level, before any platform-specific build step. That’s why one implementation reaches every port: iOS/ParparVM translates the already-hardened bytecode to C (so the C constant pool never sees the plaintext), R8 consumes already-hardened classes on Android, and the JavaScript backend minifies already-hardened classes.

TransformPortsNotes

Class / method / field renaming

iOS, JavaScript, Windows, Linux, desktop

Android keeps R8 as its sole renamer — renaming twice would only force a pointless mapping composition. The renamer uses a distinctive name dictionary on purpose: short names such as a/b are substrings of the ParparVM native identifiers and would defeat the translator’s dead-code elimination, so a six-character prefixed name is used instead.

String constant encryption

iOS, Android, Windows, Linux, desktop

Both channels are handled: the LDC string literals in method bodies and the ConstantValue attribute of static final String fields, which would otherwise leak into the ParparVM C constant pool even after the readers were encrypted. Each distinct literal is decoded once (per class) and interned, so equal encrypted literals stay reference-equal to each other. The decoder is synthesized into each class with a per-class key, so there is no single framework method to hook. Not applied on the JavaScript port, where a string literal can be a live reference into the native bridge.

One caveat: an encrypted app literal is a different object from an equal un-encrypted literal elsewhere — for example a literal of the same value returned by the framework, which isn’t hardened. Reference (==) comparison of string literals across that boundary can therefore change. Compare strings with .equals(), which is unaffected; == on distinct string values was never a guarantee to rely on.

One exclusion: a string used as an annotation value (or an annotation-method default) is stored by javac in the annotation metadata, not as an LDC or a field constant, so it’s not encrypted and remains readable in the binary. Codename One has no runtime reflection, so your app can’t read that value back anyway — but don’t place a secret in an annotation and expect it hidden.

Another exclusion: string concatenation such as "prefix=" + value, compiled by a JDK 9 or newer javac, becomes an invokedynamic whose literal fragments live in the concatenation recipe rather than an LDC or a field constant, so the encryption can’t reach them and they stay readable. The engine counts these sites and the build report lists them, so a build that ships such literals says so rather than claiming complete string encryption. Compiling with -XDstringConcat=inline (or an older -target) makes javac emit StringBuilder calls whose literals the engine does encrypt.

Control-flow obfuscation

Android, desktop

An opaque predicate guarded by a value the decompiler can’t fold. Left off the ParparVM native ports, where it fights the translator’s optimizer and the arithmetic reducer, and off JavaScript, where it inflates the bundle. Never applied to constructors.

67.2. Turning it on

Add the level to your codenameone_settings.properties:

codename1.arg.harden.level=standard
Build hintDefaultDescription

harden.level

off

Master switch: off, standard, aggressive or paranoid. An unrecognized value fails the build rather than being treated as off.

harden.rename

(level)

Override renaming on/off independently of the level.

harden.strings

(level)

off, constants, or all.

harden.controlFlow

(level)

Override control-flow obfuscation on/off.

harden.keep

(none)

Keep rules in ProGuard syntax, one rule per line, for classes resolved by name at runtime that the automatic analysis can’t see. Same syntax as android.proguardKeep, so existing rules port directly. (Rules are separated by newlines only, since a ; is legal inside a rule body such as { *; }.)

harden.<platform>.enabled

true

Per-port opt-out (and, ios, mac, linux, win, javascript, javase), mirroring the Crash Protection opt-outs. Only an explicit false disables a platform.

harden.requireSymbolUpload

true

Fail the build if the symbol/mapping upload fails. Losing a hardened build’s mapping makes its crash reports permanently unreadable, so this defaults to strict.

harden.allowUnhardenedLocalBuild

false

Escape hatch: allow a local or source-project target to build unhardened instead of failing the pre-flight.

harden.seed

(build id)

Fixes the renaming seed for a reproducible mapping across rebuilds; leave unset for a fresh mapping per build.

67.3. Levels

The level is the one decision most projects need to make. The individual switches are overrides on top of it.

offstandardaggressiveparanoid

Class/method/field renaming

 — 

yes

yes

yes

String encryption

 — 

constants

all

all

Control-flow obfuscation

 — 

 — 

yes

yes + opaque predicates

Local-variable debug stripping

 — 

yes

yes

yes

Symbol/mapping upload

 — 

required

required

required

On a renamed build the source file name (SourceFile) is stripped, matching DexGuard: the engine-renamed ports drop it, and on a minified Android release R8 drops it too. LineNumberTable is kept so a hardened crash still retraces to a line number against the mapping; the retrace reconstructs the file name from the (retraced) class name. Renaming also removes the local-variable and parameter names.

An explicitly unminified Android build (android.enableProguard=false, which android.onDeviceDebug also forces) doesn’t rename at all — only string encryption and control-flow obfuscation apply — so its class names, and SourceFile along with them, remain. That’s a debug configuration, not the release artifact hardening targets.

67.4. Keeping what must not be renamed

Renaming is safe for code the compiler and runtime resolve by symbol. Codename One has no runtime reflection — Class.forName never resolved an obfuscated application class — so there is no reflective seam to protect, and there is no serialization to keep members for. What must survive is the small set the build resolves by name: the main class and its generated stub, the generated router and annotation bootstraps, and each native interface with its generated Impl/Stub peer. The engine finds the native interfaces in the input and keeps exactly those peers, and keeps the rest of that set automatically.

PropertyBusinessObject properties are safe without any special handling: a property’s JSON key and database column come from the string passed to its Property, not from the field name, so renaming the field doesn’t change the on-disk schema or the wire format. String encryption decodes those strings back to the same value at runtime.

If you have a class the build resolves by a name the automatic analysis can’t see, add a harden.keep rule for it.

Package names are obfuscated along with class names. Codename One’s own resource loading uses absolute paths (Display.getResourceAsStream("/name"), the theme .res), which are unaffected. The one thing renaming can break is a package-relative resource lookup — getClass().getResourceAsStream("config.properties"), without a leading slash — because the runtime resolves it under the class’s now-obfuscated package while the resource stays at its original path. This is uncommon in application code, but a bundled third-party dependency might do it; if one loads a resource relative to its own package, add a harden.keep rule for that class (or its package) so its package path stays put.

67.5. Crash reports from a hardened build

Hardening and Crash Protection are designed together. The build server retains the obfuscation mapping and symbolicates incoming reports against it, so a crash from a hardened build still lands as a readable, correctly lined GitHub issue — see Chapter 77, Crash Protection. Two consequences follow: a report whose mapping has aged out of retention can no longer be retraced, and a locally hardened build (whose mapping never reached the server) can’t be symbolicated at all, which is why the pre-flight refuses to harden a local target by default.

67.6. Local and source builds aren’t hardened

Hardening runs on the Codename One build server, and only there. The engine itself is open source — it lives in this repository and ships inside the Maven plugin — so its protection isn’t the secrecy of the algorithm. What the server boundary provides is enforcement and custody: the Enterprise entitlement is checked where the account lives (a client can’t grant itself the feature), the renaming dictionary and decoder keys are seeded per build so the exact obfuscation differs each time and can’t be predicted without the build key, and the obfuscation mapping is retained server-side for symbolication rather than handed to the client.

A local or source-project target (-source, local-) never reaches the server, so its output isn’t hardened; the build fails the pre-flight rather than mislead you, unless you set harden.allowUnhardenedLocalBuild=true. The simulator is never obfuscated either — it runs your target/classes directly. App code can read com.codename1.security.hardening.Hardening.isHardened() to tell a hardened build apart from one of these.

67.7. Hardening and App Shield

These are two different Enterprise features and you can use either or both. App Hardening protects the binary — it raises the cost of reading and modifying the app on the device. App Shield protects the app-to-server relationship — it gives your backend a cryptographically verifiable statement that a request came from a genuine, unmodified app on an uncompromised device. Hardening makes an attacker work harder to patch out App Shield’s checks; App Shield makes patching them out insufficient, because the statement your backend trusts is made by a party the attacker doesn’t control.

67.8. What this doesn’t protect against

Hardening raises the cost of static analysis and casual tampering. It doesn’t stop a determined attacker with time, it doesn’t protect a secret you embed in the client (put it on your server — see the security chapter), and it’s not a substitute for server-side authorization. Treat it as one layer of defense in depth, not a guarantee.

68. Biometric Authentication

Codename One ships first-class biometric authentication (Touch ID, Face ID, Android BiometricPrompt) under the com.codename1.security package. A single entry point exposes the platform prompt, a typed enum reports the modalities currently enrolled, and a typed error enum lets callers react to failures without string matching.

A sibling SecureStorage API stores secret strings (auth tokens, refresh tokens, encryption keys) behind biometric authentication using the iOS keychain or the Android Keystore.

68.1. Quick start: Authenticate

Biometrics b = Biometrics.getInstance();
if (!b.canAuthenticate()) {
    // Fall back to password
    return;
}
b.authenticate("Unlock your account").onResult((success, err) -> {
    if (err != null) {
        BiometricError code = ((BiometricException) err).getError();
        switch (code) {
            case USER_CANCELED: /* user dismissed the prompt */ break;
            case LOCKED_OUT:    /* too many bad attempts */ break;
            case NOT_ENROLLED:  /* prompt the user to enrol in Settings */ break;
            default:            /* generic failure */ break;
        }
    } else {
        // Authenticated. Continue with the gated action.
    }
});

Always gate the call on canAuthenticate() and never on isSupported() alone. isSupported() only checks for hardware, while canAuthenticate() also requires at least one enrolled credential and that the device isn’t in a locked-out state.

68.2. Quick start: Secure storage

Entries are bound to the current set of enrolled biometrics. If the user enrols a new fingerprint or face after writing, the next get fails with BiometricError.KEY_REVOKED and the application must re-prompt for the original value and write it again.

68.3. Platform behaviour

PlatformImplementationNotes

iOS

LocalAuthentication.framework (LAContext) + Security.framework keychain

Add the ios.NSFaceIDUsageDescription build hint when targeting Face ID hardware.

Android API 29+

BiometricPrompt

Face / iris / fingerprint per PackageManager features.

Android API 23-28

FingerprintManagerCompat

Fingerprint only. Goes through the support library rather than android.hardware.fingerprint directly, which Android removed in API 37, so an app compiled against any platform still authenticates on these devices.

JavaSE simulator

Simulate → Biometric Simulation submenu

Toggle hardware availability, per-modality enrolment, and the next-call outcome.

All other ports

Non-supporting fallback

canAuthenticate() returns false; authenticate() completes with BiometricError.NOT_AVAILABLE.

Application code never needs a platform if. The API itself returns the right thing on each port. On platforms without biometric hardware, the fallback reports NOT_AVAILABLE immediately.

68.4. Build hints

The Codename One Maven plugin and the build daemon both automatically inject the platform configuration when the app’s bytecode references com.codename1.security:

  • iOS: LocalAuthentication.framework is linked into the generated Xcode project.

  • Android: <uses-permission android:name="android.permission.USE_BIOMETRIC" /> (and USE_FINGERPRINT with maxSdkVersion=28 for the legacy path) is injected into AndroidManifest.xml.

Apps that never touch the API pay nothing: no extra framework, no extra permission.

One thing the plugin won’t inject: the iOS Face ID usage description. Apple rejects placeholder text, so you must supply the build hint yourself:

ios.NSFaceIDUsageDescription=Authenticate to securely access your account

The JavaSE simulator automatically sets this hint to a default the first time biometrics is invoked, so projects developed in the simulator have a working default; you should overwrite the text before shipping.

To share keychain entries with iOS App Extensions, set both the build hint and the runtime access group:

ios.keychainAccessGroup=TEAMID123.group.com.example.app
SecureStorage.getInstance().setKeychainAccessGroup("TEAMID123.group.com.example.app");

The Team ID prefix is required; omitting it produces errSecMissingEntitlement.

68.5. Configuring the prompt

Use AuthenticationOptions to control prompt copy and security policy:

Biometrics.getInstance().authenticate(new AuthenticationOptions()
    .setReason("Authorize transfer")                // iOS localizedReason; Android title fallback
    .setTitle("Confirm payment")                    // Android only
    .setSubtitle("Stripe charge $25.00")            // Android only
    .setNegativeButtonText("Cancel")                // Android only
    .setBiometricOnly(true)                         // reject PIN / passcode fallback
    .setSensitiveTransaction(true)                  // request class-3 ("strong") biometric (Android 30+)
);

Unrecognized options are ignored, so callers can set the union without platform-checking.

68.6. Typed error handling

BiometricException.getError() returns a BiometricError:

CodeMeaning

NOT_AVAILABLE

No biometric hardware or the platform doesn’t support it

NOT_ENROLLED

Hardware present but the user hasn’t enrolled

LOCKED_OUT

Too many failed attempts; temporary

PERMANENTLY_LOCKED_OUT

User must unlock with passcode before biometrics can be used again

PASSCODE_NOT_SET

Device has no PIN / pattern configured (can’t use biometrics)

USER_CANCELED

User dismissed the prompt

SYSTEM_CANCELED

OS dismissed the prompt (app backgrounded, system pre-empted)

AUTHENTICATION_FAILED

Prompt completed but the user wasn’t recognized

KEY_REVOKED

SecureStorage entry can no longer be decrypted (re-enrolment occurred)

UNKNOWN

Anything not covered above

68.7. Cancelling an in-flight prompt

Call Biometrics.getInstance().stopAuthentication() to dismiss an active prompt; the pending AsyncResource then completes with BiometricError.USER_CANCELED. The method returns true if a prompt was cancelled, false if nothing was pending.

68.8. Listing enrolled biometrics

getAvailableBiometrics() returns a List<BiometricType> to drive UI affordances (icon, prompt copy):

  • FINGERPRINT: iOS Touch ID; Android fingerprint sensor.

  • FACE: iOS Face ID; Android face recognition (API 29+).

  • IRIS: a handful of Samsung devices; never on iOS.

  • STRONG: Android class-3 authenticator tier (false-acceptance < 1/100,000); only Android API 30+.

  • WEAK: Android class-2 authenticator tier; only Android API 30+. Can’t unlock Keystore-bound keys, so weak-only devices can’t use SecureStorage.

Never use this list to decide whether to call authenticate(); always use canAuthenticate() for that decision.

68.9. Simulator workflow

The JavaSE simulator’s Simulate → Biometric Simulation submenu lets you exercise every code path without a device:

  • Hardware Available: toggles isSupported() and canAuthenticate().

  • Face ID Enrolled / Touch ID Enrolled / Iris Enrolled: populate getAvailableBiometrics().

  • Next authenticate() Outcome: radio button that picks the outcome of the next prompt: SUCCEED, FAIL, CANCEL, LOCKED_OUT, PERMANENTLY_LOCKED_OUT, NOT_ENROLLED, PASSCODE_NOT_SET. The selection is sticky; change it any time.

When authenticate() is invoked, a small modal Swing dialog mirrors the platform prompt so the developer can see the trigger fire and click through. State persists across simulator restarts via java.util.prefs.

SecureStorage round-trips against java.util.prefs in the simulator, so you can test the full set → get → KEY_REVOKED flow end-to-end.

68.10. Security notes

The Android backing for authenticate() doesn’t simply forward the OS success callback; it binds the prompt to a single-use AES probe cipher in the Android Keystore (with setUserAuthenticationRequired(true) and, on API 24+, setInvalidatedByBiometricEnrollment(true)). On success the cipher performs a real doFinal operation. A hooked or spoofed success callback fails because the Keystore refuses the operation. This guards against the bypass class documented by CodeQL’s java/android/insecure-local-authentication rule.

69. Authentication and Identity

This chapter covers Codename One’s modern sign-in stack: OpenID Connect, Sign in with Apple, Google, Facebook, Microsoft Entra ID, Auth0 and Firebase Authentication.

The stack is rebuilt around two new primitives:

  • com.codename1.io.oidc.OidcClient — a full OpenID Connect / OAuth 2.0 client with PKCE, discovery, refresh and token persistence.

  • com.codename1.io.oidc.SystemBrowser — routes the sign-in step through the platform’s hardened sign-in surface where one exists: ASWebAuthenticationSession on iOS and Android Custom Tabs. Elsewhere it falls back to BrowserWindow, and what that means differs: the Web port opens a real top-level browser window, while JavaSE renders a JavaFX WebView inside the app. The desktop one is an embedded view, so a provider that blocks those can refuse a simulator sign-in that works on a device. The Web one has a constraint of its own — it’s a window.open() call, and it happens after the discovery request comes back rather than inside the click that started the sign-in, so a browser that has already expired the transient activation blocks it. Tell your users to allow popups for the site, or start the flow from an OidcClient you configured ahead of time instead of discover(). And point the redirect URI at a page on the app’s own origin: the port watches the popup by reading its location, the same-origin policy blocks that read once the popup navigates elsewhere, and the fallback reports the authorization URL again — so a callback hosted anywhere else never gets recognized and authorize() simply never completes. That applies to Apple’s form_post bridge too, which has to live on that origin rather than beside the Services ID.

Every provider-specific class is a thin layer on top of these primitives, so once you learn the underlying client you understand the whole stack.

The OpenID Connect stack and the two paths that leave it

Two places step outside it, and both are deliberate. AppleSignIn calls the native Apple sheet whenever the platform offers one, which on iOS 13+ means neither primitive is involved; the OpenID Connect path is what it falls back to everywhere else. And FirebaseAuth isn’t an OpenID Connect provider at all — you reach it by handing it an ID token you already obtained, which FacebookConnect can’t give you because Facebook’s flow issues an access token and nothing else.

The legacy com.codename1.io.Oauth2 class is deprecated. It opens an in-app WebBrowser, which modern identity providers (Google, Apple, Microsoft, Facebook) now refuse to render — they detect the embedded view and block the page. The new OidcClient works the same on every supported platform without that limitation. See Section 69.10, “Migrating from Oauth2” for a migration recipe.

69.1. Why the change

Identity providers tightened their rules in 2023 and 2024:

  • Google — the legacy GoogleSignIn SDK has been replaced by Google Identity Services (GIS). GIS expects PKCE and discourages embedded WebViews.

  • Apple — Sign in with Apple, mandatory for App Store apps that offer any other social login, requires a native ASAuthorizationAppleIDProvider call on iOS 13+ or a web flow with form_post response_mode.

  • Microsoft — Entra ID (formerly Azure AD) requires PKCE for public clients and forwards all browser flows through Microsoft Authenticator when installed.

  • Facebook — Facebook Login still supports OAuth 2.0 but its embedded-WebView detection now blocks unknown user agents.

The old Oauth2 class drove the entire flow through com.codename1.components.WebBrowser, which is an embedded view. That model no longer works on a Codename One device build for these providers.

69.2. Quick start — any OpenID Connect provider

If your identity provider publishes a standard .well-known/openid-configuration document (Google, Microsoft, Apple, Auth0, Okta, Keycloak, AWS Cognito, Authentik, and most others do) you can sign users in with about ten lines of code.

OidcClient.discover("https://accounts.google.com").ready(new SuccessCallback<OidcClient>() {
    public void onSucess(OidcClient client) {
        client.setClientId("YOUR_CLIENT_ID")
              .setRedirectUri("com.example.app:/oauth2redirect")
              .setScopes("openid", "email", "profile");
        client.authorize().ready(new SuccessCallback<OidcTokens>() {
            public void onSucess(OidcTokens tokens) {
                // tokens.getAccessToken() -- bearer for API calls
                // tokens.getIdToken()      -- JWT with user identity claims
                // tokens.getEmail()        -- convenience accessor
            }
        }).except(new SuccessCallback<Throwable>() {
            public void onSucess(Throwable err) {
                System.out.println("Sign-in failed: " + err.getMessage());
            }
        });
    }
});

That call:

  1. Fetches accounts.google.com/.well-known/openid-configuration.

  2. Generates a PKCE pair (S256), a state, and a nonce.

  3. Opens the OS system-browser sheet at the discovered authorization endpoint.

  4. Exchanges the code for tokens on the discovered token endpoint.

  5. Verifies state and nonce, decodes the ID token, and persists the tokens via the default TokenStore.

The sign-in flow across the app the platform browser and the provider

The token exchange is the leg worth noticing: it goes straight from your app to the provider’s token endpoint, carrying the PKCE verifier, and the browser has already done its job by then. A provider that treats your app as a public client wants no secret on that request, which is the arrangement PKCE exists to make safe. Apple is the exception on its non-native path and wants a client_secret JWT your server mints, which is what Section 69.3.3, “One-time Apple setup” is about.

Only the redirect URL crosses back from the browser, which is why Apple’s non-iOS path needs a server in front of it: AppleSignIn asks for response_mode=form_post, so Apple POSTs code and state to your redirect URI rather than putting them in the query string, and OidcClient parses the URL alone. The page at that URI has to turn the POST into a redirect carrying the same parameters, or the sign-in fails with STATE_MISMATCH.

Whether the token endpoint hands back a refresh token is up to the request: the quick start above asks for openid, email and profile and nothing more, so Google returns none and there is nothing for refreshIfExpired to renew. GoogleConnect.signIn adds access_type=offline and prompt=consent for exactly that reason.

69.2.1. Picking a redirect URI

For mobile apps, use a custom scheme unique to your app, for example com.example.app:/oauth2redirect. Register the scheme with the OS via the build hints below so the system browser can hand the redirect back to your app.

For web / simulator runs, use a normal HTTPS URL pointing at a page you control. The Codename One fallback BrowserWindow closes as soon as the URL matches.

iOS

Add the URL scheme to your Info.plist via the standard build hint:

ios.urlScheme=<key>CFBundleURLTypes</key>\n<array>\n  <dict>\n    <key>CFBundleURLSchemes</key>\n    <array>\n      <string>com.example.app</string>\n    </array>\n  </dict>\n</array>

AuthenticationServices.framework is added to the linker automatically whenever your app references anything in com.codename1.io.oidc or com.codename1.social.AppleSignIn — you don’t need to add ios.add_libs=AuthenticationServices.framework manually.

Android

Add an intent filter for the redirect scheme to the application manifest via the android.xintent_filter build hint:

android.xintent_filter=<intent-filter>\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="com.example.app"/>\n</intent-filter>

The androidx.browser:browser:1.8.0 Gradle dependency (for Custom Tabs) is added to your app’s Gradle build automatically whenever it references anything in com.codename1.io.oidc. Override the version with build hint android.customTabsVersion=1.x.y if needed.

69.2.2. Saving and restoring tokens

OidcClient saves the response under a per-issuer + per-client-ID key using com.codename1.io.oidc.TokenStore.DefaultStorageTokenStore (which serializes to com.codename1.io.Storage). To restore on next launch:

client.refreshIfExpired(60).ready(new SuccessCallback<OidcTokens>() {
    public void onSucess(OidcTokens tokens) {
        if (tokens == null) {
            // No saved tokens, or they expired and we have no refresh token.
            client.authorize();
        } else {
            // Reuse `tokens.getAccessToken()` -- still valid (refreshed if needed).
        }
    }
});

If the saved access token is within 60 seconds of expiring and a refresh token is available, refreshIfExpired performs a refresh-token grant in the background and persists the new tokens.

69.2.3. Encrypting tokens at rest

The default token store uses com.codename1.io.Storage, which is sandboxed but not encrypted. For biometric-gated storage, implement TokenStore over com.codename1.security.SecureStorage (see the source of DefaultStorageTokenStore for the pattern — swap the Storage.writeObject / readObject calls for SecureStorage.set / get).

69.3. Sign in with Apple

com.codename1.social.AppleSignIn consolidates the previous cn1-applesignin cn1lib into core. On iOS 13+ it uses native ASAuthorizationAppleIDProvider; on Android, JavaSE and the Web port it uses the public Apple OIDC issuer through OidcClient.

69.3.1. iOS

The iOS port implements the com.codename1.social.AppleSignInNative interface. No build hints are required beyond enabling the Sign in with Apple capability:

ios.signinwithapple=true
AppleSignIn.getInstance().signIn("name email", new AppleSignInCallback() {
    public void onSuccess(AppleSignInResult result) {
        // result.getIdentityToken()  -- JWT to verify on your server
        // result.getUserId()         -- stable opaque user id, use as PK
        // result.getEmail()          -- may be a real or relay address
    }
    public void onError(String err) { }
    public void onCancel()          { }
});
Apple only returns the user’s name and email on the first authorization. AppleSignIn persists those values in Preferences so the result is consistent on subsequent sign-ins.

69.3.2. Android, JavaSE, Web

Apple requires a separate Services ID (a "web client") for non-iOS environments and a client_secret JWT generated by your server. The recipe lives at Section 69.3.3, “One-time Apple setup”.

On Android there is a second thing to arrange. The Services ID callback is an HTTPS URL, and the Android sign-in surface hands control back by matching the scheme of the redirect it’s waiting for, so the com.example.app custom scheme registered earlier in this chapter does nothing for it. The callback host and path need their own verified App Link: an android.xintent_filter carrying an android:autoVerify="true" filter for android:scheme="https" on that host, and an assetlinks.json served from it. Deep-Link Routing Section has both, and says how to put that filter and the custom-scheme one in a single hint value — the hint holds one string, so an app doing both needs them together.

69.3.3. One-time Apple setup

  1. In the Apple Developer portal, create a Services ID alongside your App ID. Enable "Sign in with Apple" for both.

  2. Configure the Services ID’s Web Authentication Configuration with the primary App ID and your redirect URI.

  3. Generate an AuthKey private key in the Apple developer portal (.p8 file). Apple doesn’t allow public mobile clients to mint the client_secret JWT themselves; your backend must do it. See Apple’s docs for the JWT recipe.

69.4. Google Sign-In

com.codename1.social.GoogleConnect now offers a modern signIn(…​) method that runs entirely through OidcClient:

GoogleConnect.getInstance().signIn(
    // the iOS or Android client for a native build; the Web client
    // belongs with an https redirect, in the simulator or web port
    "YOUR_IOS_OR_ANDROID_CLIENT_ID.apps.googleusercontent.com",
    "com.example.app:/oauth2redirect",
    "openid", "email", "profile"
).ready(new SuccessCallback<OidcTokens>() {
    public void onSucess(OidcTokens t) {
        String email = t.getEmail();
        String idToken = t.getIdToken();
    }
});

signIn works on iOS, Android, JavaSE and Web with the same code, against the Web Application OAuth 2.0 client ID from Google Cloud Console. The redirect URI you supply must match one of the Authorized redirect URIs configured for that client.

The legacy doLogin() / setClientSecret(…​) path is still available for source compatibility but is no longer recommended.

69.4.1. Migrating from the legacy Google integration

If you currently rely on GoogleConnect.doLogin() with the Google Sign-In SDK on Android or GIDSignIn on iOS, you can switch to the modern flow without touching your build:

  • On Android, the legacy code uses com.google.android.gms.auth.api.Auth.GoogleSignInApi — this is the part Google deprecated. New code via signIn(…​) doesn’t require the native SDK at all.

  • On iOS, the legacy code uses GIDSignIn (Google Sign-In SDK). The new code uses ASWebAuthenticationSession via SystemBrowser, which is what Google now recommends for non-broker apps.

69.5. Facebook Login

com.codename1.social.FacebookConnect exposes both the old SDK-based doLogin() and a new SDK-free signIn(…​) that uses the system browser via OidcClient. Use the new method for the simulator, the web port, and for apps that don’t want to bundle the Facebook SDK at all:

FacebookConnect.getInstance().signIn(
    "FB_APP_ID",
    // This flow is for the simulator and the web port, where the
    // response comes back through the browser, so the redirect is an
    // https URL registered with Facebook. A native Android or iOS build
    // registers a custom scheme instead, as the OidcClient samples above
    // do -- match the redirect to the target you are building.
    "https://example.com/oauth2redirect",
    "public_profile", "email"
).ready(new SuccessCallback<OidcTokens>() {
    public void onSucess(OidcTokens t) {
        String accessToken = t.getAccessToken(); // ID token will be null
        // call https://graph.facebook.com/me with the access token for user data
    }
});

Facebook doesn’t issue OpenID-Connect ID tokens for classic OAuth flows, so getIdToken() returns null. Read user profile fields via the Graph API.

69.6. Microsoft / Entra ID

com.codename1.social.MicrosoftConnect wraps the same OIDC client against https://login.microsoftonline.com/{tenant}/v2.0. The default tenant common accepts personal and work / school accounts; pass a tenant GUID, organizations, or consumers to restrict.

Apps that need broker (Microsoft Authenticator) support for Conditional Access can bundle the MSAL SDK manually and bypass MicrosoftConnect. For the vast majority of apps the system-browser flow is sufficient and lets the simulator and web port share the same code path.

69.7. Auth0

Auth0Connect is the simplest of the providers because Auth0 is a clean OIDC provider; everything beyond withDomain is optional.

69.8. Firebase Authentication

Firebase Auth isn’t an OIDC provider — it issues Google-Identity-Toolkit-style tokens via REST endpoints. com.codename1.social.FirebaseAuth wraps those endpoints:

void firebaseEmailSignIn(String email, String password) {
    // withApiKey is not optional: without it every call completes its
    // AsyncResource with IllegalStateException instead of reaching
    // Firebase. Do it once, wherever your app initializes.
    FirebaseAuth auth = FirebaseAuth.getInstance().withApiKey("YOUR_WEB_API_KEY");
    auth.signInWithEmailAndPassword(email, password)
        .ready(user -> {
            // user.getIdToken() is a Firebase token rather than an OIDC
            // one, and it is a bearer credential: send it to your service,
            // never to the log. Anything written with Log or println ends
            // up in logcat and in device diagnostics.
            callMyBackend(user.getIdToken());
        })
        .except(err -> ToastBar.showErrorMessage(err.getMessage()));
}

For federated sign-in (Google / Apple / Microsoft as Firebase providers), first obtain an ID token via the matching *Connect class, then swap it for a Firebase session:

String clientId = "YOUR_IOS_OR_ANDROID_CLIENT_ID.apps.googleusercontent.com";
String redirectUri = "com.example.app:/oauth2redirect";
// without the api key every Firebase