Codename One Developer Guide
version DEV-SNAPSHOT, 2026-10-07
- Preface
- Getting started
- 1. Introduction
- 2. Maven Project Workflow
- 2.1. Introduction
- 2.2. Getting started
- 2.3. Updating Codename One
- 2.4. Creating project templates
- 2.5. Codename One libraries
- 3. Gradle project workflow
- Core concepts
- 4. The EDT - Event Dispatch Thread
- 5. Events
- 6. Device Input and Form Factors
- User interface
- 7. Basics: Themes, styles, components, and layouts
- 8. The components of Codename One
- 8.1. Container
- 8.2. Form
- 8.3. Dialog
- 8.4. InteractionDialog
- 8.5. Label
- 8.6. TextField and TextArea
- 8.7. TextComponent
- 8.8. Button
- 8.9. CheckBox/RadioButton
- 8.10. ComponentGroup
- 8.11. MultiButton
- 8.12. SpanButton
- 8.13. SpanLabel
- 8.14. OnOffSwitch
- 8.15. InfiniteProgress
- 8.16. InfiniteScrollAdapter and InfiniteContainer
- 8.17. List, MultiList, renderers & models
- 8.17.1. InfiniteContainer/InfiniteScrollAdapter vs. List/ContainerList
- 8.17.2. Why isn’t list deprecated
- 8.17.3. MVC in lists
- 8.17.4. Understanding MVC
- 8.17.5. Important - lists and Layout managers
- 8.17.6. MultiList & DefaultListModel
- 8.17.7. List cell renderer
- 8.17.8. Generic list cell renderer
- 8.17.9. ComboBox
- 8.18. Slider
- 8.19. Table
- 8.20. Tree
- 8.21. ShareButton
- 8.22. iOS Share Extension Authoring Helper
- 8.23. Tabs
- 8.24. MediaManager & MediaPlayer
- 8.25. ImageViewer
- 8.26. ScaleImageLabel & ScaleImageButton
- 8.27. Toolbar
- 8.28. BrowserComponent & WebBrowser
- 8.29. Text editors
- 8.30. AutoCompleteTextField
- 8.31. Picker
- 8.32. SwipeableContainer
- 8.33. EmbeddedContainer
- 8.34. MapComponent
- 8.35. Chart Component
- 8.36. Calendar
- 8.37. ToastBar
- 8.38. SignatureComponent
- 8.39. Accordion
- 8.40. Floating hint
- 8.41. Floating Button
- 8.42. SplitPane
- 9. Component selector
- 10. Rich Text and Code Editing
- 10.1. Editor architecture and backend selection
- 10.2. Focus, change events, and editability
- 10.3. Platform text input and input methods
- 10.4. Lightweight fields with EditField
- 10.5. Selection, bidirectional text, and scrolling
- 10.6. RichTextArea
- 10.7. Read-only rich text with RichTextComponent
- 10.8. CodeEditor
- 10.9. Port integration checklist
- 11. Accessibility Semantics
- 11.1. Built-in semantics
- 11.2. Roles, names, values, and state
- 11.3. Ranges and editable values
- 11.4. Custom accessibility actions
- 11.5. Grouping and virtual descendants
- 11.6. Traversal order
- 11.7. Live regions and screen changes
- 11.8. User accessibility preferences
- 11.9. Collections
- 11.10. Inspecting and testing semantics
- 11.11. Diagnosing and correcting an inaccessible screen
- 11.12. Platform mappings
- 12. Animations
- Theming and styling
- 13. Advanced theming
- 13.1. Working with UIIDs
- 13.2. Theme layering
- 13.3. Override resources in platform
- 13.4. Theme constants
- 13.5. Native theming
- 13.6. Under the hood of the theme engine
- 13.7. Understanding images and Multi-Images
- 13.8. Use millimeters for Padding/Margin and font sizes
- 13.9. Creating a great looking side menu
- 13.10. From a design to a theme
- 14. Native modern themes
- 14.1. Inheriting OS colors and fonts
- 14.2. Selecting a theme
- 14.3. Desktop themes
- 14.4. Light and dark mode
- 14.5. Material 3 color palette (Android)
- 14.6. iOS modern color palette
- 14.7. Accent palette override
- 14.8. Platform-specific UIIDs
- 14.9. Switching CheckBox / RadioButton glyphs
- 14.10. Component-specific tuning constants
- 14.11. iOS 26 Liquid Glass tab selection morph
- 14.12. Customizing in your own theme
- 14.13. Inheriting from a native UIID
- 14.14. Translucency, glass effects, and the test harness
- 15. CSS
- 13. Advanced theming
- Graphics and games
- 16. Graphics, drawing, images, and fonts
- 17. Build-Time Vector & Animation Images
- 18. 3D Graphics and Shaders
- 19. Augmented Reality
- 20. Virtual Reality and 360 Media
- 21. Game Development
- 21.1. Sample games
- 21.2. The game loop:
GameView - 21.3. Input:
GameInput - 21.4. On-screen touch controls
- 21.5. Sprites
- 21.6. Case study: The card game (flat 2D)
- 21.7. Case study: Faux-3D checkers (isometric projection)
- 21.8. 3D and perspective
- 21.9. Low latency audio:
SoundPool - 21.10. Physics
- 21.11. Putting it together: Anatomy of a game
- 21.12. Performance notes
- 22. The Game Builder
- 23. Game Assets
- Data, media and networking
- 24. File system, storage, network, and parsing
- 24.1. JAR resources
- 24.2. Storage
- 24.3. File system
- 24.4. SQL
- 24.5. Network manager & connection request
- 24.6. Debugging network connections
- 24.7. Rest API
- 24.8. Webservice wizard
- 24.9. Connection request caching
- 24.10. Cached data service
- 24.11. Externalizable objects
- 24.12. UI bindings & utilities
- 24.13. Logging & crash protection
- 24.14. Sockets
- 24.15. Properties
- 25. Deeper Network Connectivity
- 26. JSON / XML Object Mapping
- 27. Component Binding
- 28. SQLite ORM
- 29. Media and Audio
- 30. Video encoding & decoding (VideoIO)
- 31. Video capture constraints API
- 24. File system, storage, network, and parsing
- Server side
- 32. Server-side backend
- 33. Backend controllers, contracts and WebSockets
- 34. Backend beans and dependency injection
- 35. Backend testing
- 36. Backend data access and transactions
- 37. Backend sessions
- 38. Backend scheduling and background work
- 39. Backend tracing, metrics and management
- 40. Backend MCP and the agent loop
- 41. Backend performance, deployment and limits
- Device and platform services
- 42. Push notifications
- 42.1. Create and configure an application
- 42.2. Bind the client
- 42.3. Build a message
- 42.4. Send from a server
- 42.5. Migrate an existing push server
- 42.6. Learn which sends failed
- 42.7. Build audiences in the Console
- 42.8. Send a message from the Console
- 42.9. Event automations
- 42.10. Widgets and live surfaces
- 42.11. Use an application-owned push server
- 42.12. Testing delivery behavior
- 43. Notifications and Background Execution
- 44. Calendar integration
- 45. External Surfaces: Widgets and Live Activities
- 46. Maps
- 47. In-Car Experiences (CarPlay & Android Auto)
- 48. Motion Sensors
- 49. Near-Field Communication (NFC)
- 50. Bluetooth
- 51. Nearby Devices
- 52. Call Management
- 52.1. Five Things Worth Knowing Before You Design Around This
- 52.2. Owning A Call
- 52.3. The Values These APIs Hand You
- 52.4. Actions, And Why They Have To Be Answered
- 52.5. Ringing When The App isn’t Running
- 52.6. Naming And Blocking Other People’s Callers
- 52.7. Developing Without Hardware
- 52.8. Build Hints
- 53. VPN
- 54. Health
- 54.1. Quick start
- 54.2. Permissions, and the one thing that will surprise you
- 54.3. Reading samples
- 54.4. Aggregates and time buckets
- 54.5. Writing samples
- 54.6. Background delivery
- 54.7. Workouts
- 54.8. Bluetooth health sensors
- 54.9. Nutrition
- 54.10. Build configuration
- 54.11. Privacy and store policy
- 54.12. Simulating health
- 54.13. Troubleshooting
- 55. Smart Home
- 55.1. The model
- 55.2. Quick start
- 55.3. Android’s default answer isn’t "available"
- 55.4. Reading and writing
- 55.5. Values and units
- 55.6. Watching for change
- 55.7. Scenes
- 55.8. Adding an accessory
- 55.9. Build hints
- 55.10. Previewing in the simulator
- 55.11. Not claimed in this release
- 55.12. Commissioning onto your own fabric
- 56. Printing
- 57. Deep-Link Routing
- 58. State Restoration and Continuity
- 59. App Intents: Siri, Spotlight and Shortcuts
- 59.1. Three nouns
- 59.2. Declaring an intent
- 59.3. Intents and routes are different questions
- 59.4. Entities: The part that makes intents feel intelligent
- 59.5. Publishing content to device search
- 59.6. The headless contract
- 59.7. What each platform actually does
- 59.8. The floor everything else sits on
- 59.9. Testing without a device
- 59.10. What this costs your iOS deployment target
- 59.11. Zero cost when unused
- 60. Document Provider
- 61. Apple Wallet Extension
- 62. Miscellaneous features
- 62.1. Phone functions
- 62.2. Larger text accessibility
- 62.3. Contacts API
- 62.4. Localization & internationalization (L10N & I18N)
- 62.5. Location - GPS
- 62.6. Background music playback
- 62.7. Capture - photos, video, audio
- 62.8. Gallery
- 62.9. Analytics integration
- 62.10. Social sign-in (Facebook, Google, …)
- 62.11. Lead Component
- 62.12. Pull to refresh
- 62.13. Running 3rd party apps using display’s execute
- 62.14. Automatic build hint configuration
- 62.15. Easy thread
- 62.16. Mouse cursor
- 62.17. Working with git
- 42. Push notifications
- AI and agents
- 63. AI, Chat UI, and Speech
- 63.1. The
com.codename1.aipackage- 63.1.1. A first chat
- 63.1.2. Streaming tokens
- 63.1.3. Tool calling
- 63.1.4. Structured output
- 63.1.5. Multi-modal messages
- 63.1.6. Embeddings
- 63.1.7. Image generation
- 63.1.8. Conversation persistence
- 63.1.9. Prompt templates
- 63.1.10. Retry policy
- 63.1.11. Simulator redirect for offline development
- 63.1.12. Storing the API key
- 63.2. The
ChatViewcomponent - 63.3. Speech recognition and TextToSpeech
- 63.4. On-device vision
- 63.5. Portable LiteRT inference
- 63.6. On-device language services
- 63.7. Platform availability
- 63.8. Migrating from the AI cn1libs
- 63.1. The
- 64. MCP Headless API
- 63. AI, Chat UI, and Speech
- Security and identity
- 65. Security
- 65.1. Constant obfuscation
- 65.2. Storage encryption
- 65.3. Secure storage and secrets
- 65.4. Vaults: One password, every device
- 65.4.1. Setting one up
- 65.4.2. Storing things
- 65.4.3. Unlock policies
- 65.4.4. Measured browser support
- 65.4.5. Asking what actually protects it
- 65.4.6. What a vault protects against
- 65.4.7. Keys you can use and can’t read
- 65.4.8. Encrypted databases
- 65.4.9. Cross-platform interoperability
- 65.4.10. Deploying the browser build
- 65.5. Disabling screenshots
- 65.6. Blocking copy & paste
- 65.7. Blocking jailbreak
- 65.8. Device integrity, RASP & attestation
- 65.9. Strong Android certificates
- 65.10. Certificate pinning
- 65.11. Cryptographic primitives
- 65.11.1. Hashing
- 65.11.2. Message authentication (HMAC)
- 65.11.3. Secure random numbers
- 65.11.4. Symmetric encryption (AES)
- 65.11.5. Asymmetric encryption and digital signatures (RSA, ECDSA)
- 65.11.6. JSON Web Tokens
- 65.11.7. HOTP / TOTP one-time passwords
- 65.11.8. Walkthrough: Adding two-factor authentication to a sign-in flow
- 65.11.9. OTP input widget
- 65.11.10. Algorithm identifier reference
- 65.11.11. Key sizes and recommended defaults
- 66. App Shield
- 66.1. How it fits together
- 66.2. Turning it on
- 66.3. Protecting hosts
- 66.4. What happens to a request
- 66.5. Status, and what to do about it
- 66.6. Getting a token yourself
- 66.7. Certificate pinning
- 66.8. Runtime signals
- 66.9. Transports the guard doesn’t cover
- 66.10. Composing with your own network guard
- 66.11. Verifying the token in your backend
- 66.12. iOS App Attest specifics
- 66.13. Testing it in the simulator
- 66.14. When there is no engine
- 67. App Hardening
- 68. Biometric Authentication
- 69. Authentication and Identity
- 69.1. Why the change
- 69.2. Quick start — any OpenID Connect provider
- 69.3. Sign in with Apple
- 69.4. Google Sign-In
- 69.5. Facebook Login
- 69.6. Microsoft / Entra ID
- 69.7. Auth0
- 69.8. Firebase Authentication
- 69.9. Passkeys / WebAuthn
- 69.10. Migrating from
Oauth2 - 69.11. Verifying a phone number
- 69.12. A universal sign-in demo
- 70. Phone Number Verification
- 65. Security
- Monetization and analytics
- 71. Commerce
- 72. Monetization
- 72.1. In-app purchase
- 72.1.1. The SKU
- 72.1.2. Types of products
- 72.1.3. The "hello world" of in-app purchase
- 72.1.4. Making it consumable
- 72.1.5. Non-Renewable subscriptions
- 72.1.6. Promotional offers (iOS)
- 72.1.7. Restoring purchases and managing subscriptions
- 72.1.8. The Server-Side
- 72.1.9. The "hello world" of Non-Renewable subscriptions
- 72.1.10. Subscribe() vs purchase()
- 72.1.11. Screenshots
- 72.1.12. Summary
- 72.1.13. Auto-Renewable subscriptions
- 72.1.14. Auto-Renewable vs Non-Renewable: Choosing between them
- 72.1.15. Learning by example
- 72.1.16. Building the IAP demo project
- 72.1.17. Looking at the source of the app
- 72.1.18. The CN1-IAP-Validator library
- 72.1.19. The
validateAndSaveReceipt()method - 72.1.20. Setting up the stores
- 72.1. In-app purchase
- 73. Advertising
- 74. Analytics
- 75. Distributed tracing with OpenTelemetry
- 76. App Review & Feedback
- 77. Crash Protection
- Testing, performance and debugging
- 78. Testing with JUnit 5
- 78.1. When to use JUnit versus AbstractTest
- 78.2. Dependencies
- 78.3. A minimal JUnit test
- 78.4.
@CodenameOneTest - 78.5.
@RunOnEdt - 78.6.
@SimulatorProperty/@SimulatorProperties - 78.7.
@Theme - 78.8.
@DarkMode - 78.9.
@LargerText - 78.10.
@Orientation - 78.11.
@RTL - 78.12. Annotation resolution
- 78.13. EDT semantics
- 78.14. Headless behavior
- 78.15. Coexistence with
cn1:test - 78.16. Side-by-side example
- 78.17. Cross-reference
- 79. Performance, size & debugging
- 79.1. Reducing resource file size
- 79.2. Improving performance
- 79.3. Profile-guided optimization
- 79.4. SIMD: Data-Parallel primitives
- 79.4.1. When to use SIMD
- 79.4.2. Getting an instance
- 79.4.3. Allocation: Alignment and provenance
- 79.4.4. A minimal End-to-End example
- 79.4.5. Scratch allocations:
alloca* - 79.4.6. Rules for
alloca*scratch arrays - 79.4.7. Build-Time verification
- 79.4.8. Simulator tracking of aligned arrays
- 79.4.9. Built-In SIMD paths
- 79.5. Performance monitor
- 79.6. Network speed
- 79.7. Debugging Codename One sources
- 79.8. Device testing Framework/Unit testing
- 79.9. EDT error handler and sendlog
- 79.10. Kitchen sink case study
- 80. On-Device Debugging (iOS)
- 80.1. When to use it
- 80.2. Quick start (IntelliJ IDEA)
- 80.3. Quick start (NetBeans)
- 80.4. Quick start (Maven from the command line)
- 80.5. What works today
- 80.6. Pointing the IDE at the Codename One sources
- 80.7. Known limitations
- 80.8. Performance
- 80.9. Debugging the generated Xcode project natively
- 80.10. Troubleshooting
- 80.10.1. The proxy reports "device disconnected" before the breakpoint fires
- 80.10.2. "Connection refused" when the device tries to reach the proxy
- 80.10.3. jdb reports "Internal exception: Unexpected JDWP Error: 100"
- 80.10.4. The app launches but the breakpoint never fires
- 80.10.5. Make the app wait while the proxy and IDE attach
- 81. On-Device Debugging (Android)
- 81.1. When to use it
- 81.2. Quick start (IntelliJ IDEA)
- 81.3. Wireless debugging
- 81.4. Quick start (Maven from the command line)
- 81.5. Useful command-line flags
- 81.6. What you can step through
- 81.7. Pointing the IDE at the Codename One sources
- 81.8. Debugging the generated Gradle project from Android Studio
- 81.9. Troubleshooting
- 78. Testing with JUnit 5
- Build, sign and ship
- 82. Signing, certificates & provisioning
- 82.1. Common terms in signing & distribution
- 82.2. Certificate Wizard
- 82.2.1. Creating the App Store Connect API key
- 82.2.2. Recommended fast path: Auto Setup
- 82.2.3. Mac signing and notarization
- 82.2.4. Android signing
- 82.2.5. Windows signing
- 82.2.6. Clearing cached signing data
- 82.2.7. Manual asset management
- 82.2.8. Selecting devices
- 82.2.9. Decisions & edge cases
- 82.2.10. App IDs and provisioning profiles
- 82.2.11. Installing files locally
- 82.2.12. Building your app
- 82.3. Advanced iOS signing
- 82.4. Android
- 83. App Store Submission and Deployment
- 82. Signing, certificates & provisioning
- Platform guides
- 84. Working with iOS
- 84.1. Troubleshooting iOS debug build installs
- 84.2. Launch screen best practices
- 84.3. Local notifications on iOS and Android
- 84.4. iOS beta testing (testflight)
- 84.5. Metal renderer
- 84.6. Choosing a color space for the Metal renderer
- 84.7. Accessing insecure URLs
- 84.8. Using Cocoapods
- 84.9. Using Swift package manager
- 84.10. Including dynamic frameworks
- 85. Working with JavaScript
- 85.1. Choosing the JavaScript compiler
- 85.2. Troubleshooting build errors
- 85.3. ZIP, WAR, or preview: What’s the difference
- 85.4. Setting up a proxy for network requests
- 85.5. Using the CORS proxy for same origin requests
- 85.6. Customizing the splash screen
- 85.7. Debugging
- 85.8. Including Third-Party JavaScript libraries
- 85.9. Browser environment variables
- 85.10. Changing the native theme
- 85.11. Page settings
- 85.12. Selecting and copying text
- 85.13. Disabling the 'OnBeforeUnload' handler
- 85.14. Deploying as a progressive web app
- 85.15. Playing media and opening links
- 85.16. BrowserComponent lifecycle
- 86. Working with macOS
- 87. Working with Mac Catalyst
- 88. Working with the native Windows port
- 89. Working with the native Linux port
- 90. Desktop Integration
- 91. Desktop Windows
- 91.1. Where windows exist
- 91.2. Window and Form
- 91.3. Window lifecycle
- 91.4. Window chrome and geometry
- 91.5. Styling
- 91.6. Modal windows
- 91.7. Monitors and per-monitor DPI
- 91.8. Window events
- 91.9. Peer components and native text editing
- 91.10. Overlays inside a window
- 91.11. Dialogs in a window of their own
- 91.12. Transitions in a window
- 91.13. Unsupported inside a window
- 91.14. Capturing a window
- 91.15. macOS
- 91.16. Mac Catalyst (legacy)
- 91.17. Threading
- 92. Wearables (Apple Watch and Wear OS)
- 92.1. One Project, Two Apps
- 92.2. Companion or Standalone
- 92.3. Running the Pair While You Develop
- 92.4. Detecting the Watch Form Factor
- 92.5. Designing for the Watch
- 92.6. Sharing Data Between the Phone and the Watch
- 92.7. Complications and Tiles
- 92.8. Apple Watch (watchOS)
- 92.9. Android (Wear OS)
- 92.10. Feeding a Complication from the Phone
- 92.11. Summary
- 93. Television (Apple TV and Android TV)
- 84. Working with iOS
- Under the hood
- 94. Advanced topics/under the hood
- 94.1. Sending arguments to the build server
- 94.2. Versioned builds
- 94.3. Android permissions
- 94.4. On device debugging
- 94.5. Native interfaces
- 94.6. Libraries - cn1lib
- 94.7. Integrating Android 3rd party libraries & JNI
- 94.8. Integrating iOS 3rd party libraries
- 94.9. Drag & drop
- 94.10. Coloring the native Android chrome
- 94.11. Intercepting URLs on iOS & Android
- 94.12. Native peer components
- 94.13. Building your own Layout manager
- 94.14. Update framework
- 95. Java in the browser: The Playground compiler and the self-hosted translator
- 96. Working with Codename One sources
- 97. Skin Designer
- 94. Advanced topics/under the hood
- Appendices
- 98. Promises
- 99. Plugins
- Appendix A: Project archetypes
- Appendix B: Maven goals
- B.1. Codename One build (
build) - B.2. Generate legacy cn1lib (
cn1lib) - B.3. Compliance check (
compliance-check) - B.4. Compile CSS (
css) - B.5. Generate app project (
generate-app-project) - B.6. Clone project (
clone) - B.7. Create GUI Form (
create-gui-form) - B.8. Generate cn1lib project (
generate-cn1lib-project) - B.9. Generate desktop app wrapper (
generate-desktop-app-wrapper) - B.10. Generate GUI sources (
generate-gui-sources) - B.11. Generate native interfaces (
generate-native-interfaces) - B.12. Generate OpenAPI client (
generate-openapi) - B.13. Generate gRPC client (
generate-grpc) - B.14. Generate GraphQL client (
generate-graphql) - B.15. GUIBuilder goal (
guibuilder) - B.16. Generate archetype (
generate-archetype) - B.17. Install Codename One (
install-codenameone) - B.18. Install legacy Cn1lib (
install-cn1lib) - B.19. Open Designer (
designer) - B.20. Prepare simulator classpath (
prepare-simulator-classpath) - B.21. Open settings (
settings) - B.22. Run tests (
test) - B.23. Update Codename One (
update)
- B.1. Codename One build (
- Appendix C: API
- Appendix D: Codename One Settings
- Appendix E: Rich properties file (rpf) format
- Historical reference
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:
Codename One Javadoc — the complete API reference.
“How Do I?” video tutorials — short screencasts for common tasks.
The Codename One blog — release notes and deep dives.
The community discussion forum — questions and answers.
The Codename One repository — source and issue tracker.
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:
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
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.
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.
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.
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

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!

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.

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=falseThis 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. |
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
Nativeas it’s a minimal starting point
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.
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:
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);
}
}| 1 | This 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. |
| 2 | runApp() is where the app builds its first screen. Lifecycle calls it
from start(), so this is the one method a new project overrides. |
| 3 | Form 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. |
| 4 | The 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 globalToolbar, binds crash protection throughLog.bindCrashProtectionand registers a network error listener, in that order.start()callsrunApp()on a cold start. On a warm start it re-shows theFormthe user was looking at when the app was suspended, so returning to an app lands the user back where they were.stop()remembers thatForm, disposing aDialogfirst 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.
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:
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());
}
}| 1 | Without this none of the five things init does happen, most visibly the
theme, so the app comes up unstyled. |
| 2 | Called as the app is suspended, which is the moment to persist anything the user would hate to lose. |
| 3 | The 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

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:settingsUse 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 |

Your certificate will generate into the file Make sure to back that up and the password as losing these can have dire consequencesKeychain.ks in your home directory |
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.

You need 4 files for signing. Two certificates and two provisioning profiles:
Production: The production certificate/provisioning pair is used for builds that are uploaded to iTunes
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:certificatewizardAfter 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.
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:

| 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 -vBoth 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.

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 starter projects are based on the Section A.1, “Codename One application project archetype (cn1app-archetype)”.
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=falseThis 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:
The
com.codenameone:codenameone-maven-plugin:7.0.210:generate-app-projectargument is the fully qualified goal name for thegenerate-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.The
archetypeGroupId,archetypeArtifactId, andarchetypeVersionparameters are the same as when using thearchetype:generategoal, and they will (almost) always refer to the Section A.1, “Codename One application project archetype (cn1app-archetype)”.The
groupId,artifactId, andversionwork the same as for thearchetype:generategoal. That’s, that they specify the coordinates for your newly created project.The
mainNamespecifies 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"The
sourceProjectproperty 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.shOn 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"
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"
button on the toolbar.
If all goes well it should open in the Codename One simulator.
Eclipse IDE
Open Eclipse, and select "File" > "Import…"

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

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

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

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.

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

Then check the eclipse option, and press 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:

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”. |
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 The correct 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
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.
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.txtThe 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.cn1lib2.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:updateCLI
Or you can use the run.sh/run.bat script to run this goal as follows:
./run.sh updateUse run.bat update instead on Windows |
IntelliJ
Or you can click on the "Configuration" menu, and select "Tools" > "Update Codename One" as shown here:

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

Then press the
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.mainNameproperty in the project’scodenameone_settings.propertiesfile.- $YOUR_PROJECT_PACKAGE_NAME
This should be the value of the
codename1.packageNameproperty in the project’scodenameone_settings.propertiesfile.- … 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:
Cross-platform Java classes.
Native code that targets specific platforms.
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.
CSS files.
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 And then follow the prompts. Or you could enter:
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
Select "File" > "New Project"…

Select "Maven" in the left menu.

Check the "Create from Archetype" checkbox
. This should allow you to choose from the archetypes that are already known to IntelliJ.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.
Fill in this dialog as shown in the above image. Specifically
groupId=com.codenameone,artifactId="cn1lib-archetype," andversion="LATEST"Then press
OK.Select the option that says "com.codenameone:cn1lib-archetype"

Then press "Next"
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.

Then click "Next"
The final form in this wizard summarizes the project details and gives you an opportunity to add more properties to pass to the
archetype:generategoal. In your case you don’t need to add any more properties. If the information looks correct, you can pressNext.
At this point you will be prompted to open the project.
NetBeans
Select "File" > "New Project…"

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

Then press "Next"
This will bring you to the "Maven Archetype" dialog as shown below:

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"
This will bring you to the "Name and Location" panel of the wizard.

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
Select "File" > "New Project…"
In the New Project dialog, expand the Maven item, and select Maven Project

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

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:

Select that option, and press Next
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.

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:

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:

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:

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:

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

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:

Eclipse IDE
The package explorer will look like:

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.xmlThis 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.versionproperty, which specifies the version of the Codename One libraries that the module should be compiled against. Periodically, you’ll want to update thecn1.versionproperty 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 installIntelliJ IDEA
Press the "build"
button on the toolbar.
NetBeans
Right-click on the "root" module in the project explorer and select 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"
button on the toolbar.
Eclipse IDE
Right-click on the "root" module in the project explorer and select Run as > Maven Install

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: |
|
artifactId: |
|
version: |
|
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#primeReadFailureRequired keys:
nameMenu title shown in the simulator’s menu bar. One menu per properties file.
itemNA
fully.qualified.ClassName#staticMethodNamereference for the Nth menu item. The method must bepublic static voidand take no arguments. Items are numbered from 1 upward; the loader stops at the first missingitemN, so don’t leave gaps.
Optional keys:
namespaceIdentifier used for the
CN.executelookup (for example,bluetoothfor a URL likebluetooth:item1). Defaults to lowercased, ASCII-slugifiedname(Push Notifications!→push-notifications). Set this explicitly when you want a different identifier from the display name.labelNDisplay text for the matching
itemN. Omit entirely to make the hook API-only — registered withCN.executebut 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#primeReadFailureWhen 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,KeyStrokeand 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.propertiesfiles 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 useCN.invokeAndBlockso 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
.cn1libfiles 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 runThe 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=truecn1.sourceProjectThe project to convert. Defaults to the project Maven runs in, else the working directory.
cn1.outputDirWhere the Gradle project goes. Defaults to
<project>-gradlebeside the source project. It must not exist or must be empty.cn1.includeBackendCarries 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.gradleVersionThe plugin version the new
settings.gradle.ktsdeclares. 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=false3.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=trueTo 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/repository3.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 runnerThe 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 locallycn1Build 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=trueGradle 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 task | Maven goal | What it does |
|---|---|---|
|
| Runs the application in the simulator. |
|
| Runs the simulator suspended, waiting for a debugger on port 5005. |
|
| Compiles |
|
| Turns SVG and Lottie assets into Java sources. |
|
| Generates sources from GUI builder XML and CodeRAD view templates. |
|
| Compiles the application and its simulator native code; the simulator’s hot reload runs it. |
|
| Writes the files the simulator reads at startup. |
|
| Runs the unit tests in the simulator’s test runner. |
|
| Writes implementation stubs for every native interface; see Section 3.5, “Native interfaces”. |
| (none) | Runs before each build task and fails when a native interface has no implementation for that platform. |
|
| An Android cloud build; an Android Studio project generated locally. |
|
| An iOS debug or App Store cloud build; an Xcode project generated locally. |
|
| A native macOS build; a macOS desktop (JVM) build. |
|
| A Windows desktop (JVM) build; a native Windows build. |
|
| A native Linux build. |
|
| A JavaScript cloud build; the JavaScript port built locally. |
|
| The build given by |
|
| Opens Codename One Settings, the GUI Builder, the Game Builder or the iOS Certificate Wizard for this project. |
| (none) | Adds a |
|
| Runs a backend on this JVM. |
|
| Builds a backend as a single native binary. |
|
| Runs a backend’s tests as a native binary. The standard |
|
| Updates the plugin version in |
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=trueThe 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.
Declare the interface in
src/main/java, as described in native interfaces:public interface MyNative extends NativeInterface { String helloWorld(String hi); }Generate the stubs:
./gradlew generateNativeInterfacesThe task compiles the application, finds every interface that extends
NativeInterface, and writes a stub for each platform intosrc/<platform>/<lang>, creating only the directories it writes to. An existing implementation is never overwritten unless you pass-Pcn1.overwrite=true.Implement the stubs. The Java SE implementation in
src/javase/javacompiles 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=trueBefore 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 declaresThe 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-commonThe compiled library, with
codenameone_library_appended.propertiesandcodenameone_library_required.propertiesunderMETA-INF/codenameone/.N-common, classifiercn1cssThe 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-libA
pomthat applications depend on, with onecodename1.platformprofile 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 binaryA 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/MyServiceCN1_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 Wizardcn1Update 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=VERSION3.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:
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.:
A user added an action listener to show a Dialog.
A framework the user installed added some logging to the button.
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:
Blocking the EDT thread so the UI performance is considerably slower.
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:

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:
First event fires
It shows a blocking dialog or invokes an "AndWait" API
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.
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.
DataChangedListenerfires 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:
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.
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).
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 between0.0and1.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_CLOSEDorPOSTURE_UNKNOWN.getHingeAngle()- hinge angle in degrees (-1when unknown).getFoldOrientation()-FOLD_ORIENTATION_VERTICAL,FOLD_ORIENTATION_HORIZONTALorFOLD_ORIENTATION_NONE.getFoldBounds(Rectangle)- the region the hinge occludes, in display coordinates, or null when there is no separating fold. Treat it likeForm.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 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.

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:

Content Pane - this is the body of the
Form. When you add aComponentto theForm, 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
Toolbarclass.Toolbarsits at the top of the form and handles the title design. The title area has two parts:Title of the
Formand 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.
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.
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 thingsFlowLayout 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. |


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:
| Layout | Scrollable |
|---|---|
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) |
Layouts can be divided into two distinct groups:
Constraint Based -
BorderLayout(and a few others such asGridBagLayout,MigLayout, andTableLayout)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:
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:
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 |
Terse syntax
Almost every layout allows you to add a component using many variants of the add method:
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"));| 1 | Regular add |
| 2 | addAll accepts many components and adds them in a batch |
| 3 | add 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:
Container boxY = BoxLayout.encloseY(cmp1, cmp2); (1)
Container boxX = BoxLayout.encloseX(cmp3, cmp4);
Container flowCenter = FlowLayout. (2)
encloseCenter(cmp5, cmp6);| 1 | Most layouts have a version of enclose to encapsulate components within |
| 2 | FlowLayout 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 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.



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

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"));
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.

FlowLayout vs. BoxLayout.X_AXISBoxLayout 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 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 You can use the static import of the CN classCN 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:

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:

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"));
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 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"));

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 The TableLayoutTableLayout 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();
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 columnThe 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
| The column for the table cell. This defaults to -1 which will place the component in the next available cell |
| Like column, defaults to -1 as well |
| The column width in percentages, -1 will use the preferred size. -2 for width will take up the rest of the available space |
| Like width but doesn’t support the -2 value |
| The cells that should be occupied horizontally defaults to 1 and can’t exceed the column count - current offset. |
| Like spanHorizontal with the same limitations |
| 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 |
| Like horizontalAlign can be one of |
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"));
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();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 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:
You’re adding a layered layout to the form
You’re creating a layered layout and placing two components within. This would be the equivalent of creating a
LayeredLayoutContainerand invokingaddtwiceYou use
FlowLayoutto position theXclose 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:

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:
"1mm"- Sets 1mm insets on all sides."1mm 2mm"- Sets 1mm insets on top and bottom; 2mm on left and right."1mm 10% 2mm"- Sets 1mm on top, 10% on left and right, and 2mm on bottom."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:

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 - yThe 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:

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:
For a top inset:
referencePosition == 0 ⇒ the inset is measured from the top edge of the reference component.
referencePosition == 1 ⇒ the inset is measured from the bottom edge of the reference component.
For a bottom inset:
referencePosition == 0 ⇒ the inset is measured from the bottom edge of the reference component.
referencePosition == 1 ⇒ the inset is measured from the top edge of the reference component.
For a left inset:
referencePosition == 0 ⇒ the inset is measured from the left edge of the reference component.
referencePosition == 1 ⇒ the inset is measured from the right edge of the reference component.
For a right inset:
referencePosition == 0 ⇒ the inset is measured from the right edge of the reference component.
referencePosition == 1 ⇒ the inset is measured from the left edge of the reference component.

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.

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;
}
If you’re porting newer Matisse code there are simple changes you can do:
Change
addComponenttoaddChange
addGrouptoaddRemove references to
ComponentPlacementand referenceLayoutStyledirectly
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;
}
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
addmethod in Codename One can be changed as shown in the sample above.The constraint argument for Codename One
addcalls appears before theComponentinstance.
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:
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:
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 manipulationgetStyle() 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:
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-formgenerates a new GUI form: the.guiXML file plus the matching Java source.cn1:guibuilderopens 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.MyFormThis generates two files inside the common module:
common/src/main/guibuilder/com/example/MyForm.guicommon/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.MyFormProjects 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

cn1:create-gui-formThe window has three columns:
Project forms and palette — on the left. The upper half lists every
.guifile 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 Auto layout mode is built on the inset support in LayeredLayoutLayeredLayout. 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.

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.

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.

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,addandremove).You can’t cast it to the type it relates to. For example, you can’t cast
MultiButtontoButton.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 underlyingButton. To work around that, use ActionEvent.getActualComponent().
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:
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.

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.


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:
Toolbarand the status bar placeholder respect the top safe margins so that titles and action buttons remain legible around notches.Tabs,Sheet, andFloatingActionButtoninstances 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
safeAreaflag, 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 areasThe 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:
ContentPaneis lowestLayeredPaneis secondGlassPaneis 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);
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);
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();
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();
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();
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);
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();
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.pngEach 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);
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 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();
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
TextFieldDataChangedListener is available in
TextField. This is crucial for character by character input event trackingDone listener is available in the
TextFieldDifferent
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();
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:


| 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:
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:
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:
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:
textComponentErrorColora 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 changetextComponentErrorLineBorderBooltoggles the material-style underline that appears on validation errors. Set it tofalseif you prefer to supply a different border when errors are showntextComponentOnTopBooltoggles 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 theonTopMode(boolean)method inInputComponent, but the layout will use the theme constanttextComponentAnimBooltoggles the animation mode which again can be manipulated by a method inInputComponent. If you want to keep the UI static without the floating hint effect set this to false. Notice this defaults to true on AndroidtextComponentFieldUIIDsets the UIID of the text field to something other thanTextFieldthis 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 theTextComponentand make it different from the regular text fieldinputComponentErrorMultilineBoolmakes 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:

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"));
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:

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:

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();
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();

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();
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")));
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
ComponentGroupits a special caseUIID.
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:
With one element in a component group it will have the UIID:
GroupElementOnlyWith two elements in a component group they will have the UIID’s
GroupElementFirst,GroupElementLastWith three elements in a component group they will have the UIID’s
GroupElementFirst,GroupElement,GroupElementLastWith 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();
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();
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();
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);
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();
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();
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:

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();| 1 | Placeholder is essential for the URLImage class which this guide covers at a different place. |
| 2 | The 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. |
| 3 | This 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. |
| 4 | If there is no more data you call the addMoreComponents method with a false argument. This indicates that there is no more data to fetch. |
| 5 | Here 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. |
| 6 | You 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 aVector/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.

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:
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).
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.
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.
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.
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();
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;
}
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();
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()));
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:
ComboBoxComboBoxItemComboBoxFocusPopupContentPanePopupItemPopupFocus
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 dialogpopupCancelBodyBool- Adds a cancel button into the popup dialogcenteredPopupBool- shows the popup dialog in the center of the screen instead of under the popupotherPopupRendererBool- Uses a different list cell render for the popup than the one used for theComboBoxitself. When this isfalsePopupItem&PopupFocusbecome irrelevant. Notice that the Android native theme defines this totrue.
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();
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.

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();
}
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();
| 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();
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();

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. |


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:

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. |

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");
ShareButton behaves differently on the device… |

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.packageNameis the chosen target’s identifier: an Android package name (for example,com.whatsapp) or, on iOS, aUIActivityTypesuch ascom.apple.UIKit.activity.PostToTwitter. It may benullwhen 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 theNSExtensiondictionary, activation rules (SupportsText/SupportsWebURL/SupportsImage), and the principal class (ShareViewController).<ExtensionName>.entitlements— declarescom.apple.security.application-groupswith the configured App Group identifier.ShareViewController.swift— aSLComposeServiceViewControllersubclass that extracts text / URL / image attachments fromextensionContext, packs them into a dictionary, and writes that dictionary toUserDefaults(suiteName: appGroupId)under the keycn1.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 wherekindis"text","url"or"image". For images,valueis a file URL inside the App Group container.timestamp— aDoubleUNIX 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:
extensionNamemust be a non-empty identifier (letters, digits,_,-). It becomes the Xcode target name and the.appexbundle name. The extension’s bundle id is set to<hostBundleId>.<extensionName>by the iOS build pipeline.appGroupIdmust start withgroup.(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,acceptImagesmust 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);
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;
}
});

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 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. |


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);
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|falseimageviewerThumbnailsBool=true|falseimageviewerThumbnailBarHeightMM=<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();
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();
| 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:
form.setToolbar(new Toolbar());- allows you to activate theToolbarto a specificFormand not for the entire applicationToolbar.setGlobalToolbar(true);- enables theToolbarfor all the forms in the appTheme constant
globalToobarBool- this is equivalent toToolbar.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 -
addCommandToLeftBarRight side of the title -
addCommandToRightBarSide menu bar (the drawer that opens when you click the icon on the top left or swipe the screen from left to right) -
addCommandToSideMenuOverflow 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();


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();| 1 | You 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. |
| 2 | You use the DataChangedListener to update the search results as you type them. |
| 3 | Hidden & 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. |
| 4 | The 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. |


8.27.1. Search mode
While you can implement search manually, using the built-in search offers a simpler and uniform UI.

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();


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);
| 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;
});

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();
| 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:
| Java Type | JavaScript Type |
|---|---|
|
|
|
|
|
|
|
|
|
|
| Not Allowed |
| JavaScript Type | Java Type |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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();
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);
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;
}
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();| 1 | you 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 |
| 2 | You create the renderer UI instantly in the fields with the helper methods for wrapping elements which is pretty cool & terse |
| 3 | In a renderer it’s important to always set the value if you don’t have a value in place |

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 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_SPINNERPicker.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);
}
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.

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/

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:
Simulator
Android
iOS
8.35.2. Features
Built-in support for many common types of charts including bar charts, line charts, stacked charts, scatter charts, pie charts and more.
Pinch Zoom - The ChartComponent class includes optional pinch zoom support.
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.












| 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:
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.modelspackage. Essentially, this is where you add the data that you want to display.Set up a renderer. You can create a renderer for your chart using one of the existing renderer classes in the
com.codename1.charts.rendererspackage. The renderer allows you to specify how the chart should look. For example: the colors, fonts, styles, to use.Create the Chart View. Use one of the existing view classes in the
com.codename1.charts.viewspackage.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();
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();
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();
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();
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();
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();
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();
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());
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:

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%");
}
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:
CSS-like Selection Syntax - Support for a CSS-like syntax for "selecting" Components to be included in a set.
Fluent API - The API is fluent, meaning you can chain multiple method calls together and reduce typing.
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.
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:
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.You can explicitly wrap one or more components. For example,
$(myButton, myLabel, myCalendar)= A set with 3 components,myButton,myLabel, andmyCalendar.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.
$("Label")- The set of all components on the form with UIID "Label"$("#MyField")- The set of all components on the form with NameMyField.$(".some-tag")- The set of all components on the form with the tag "some-tag." (You’ll discuss tags below).$("Label#MyField")- The set of all components on the form with both UIID=Labeland Name=MyField.$("Label#MyField.some-tag")- The set of all components on the form with UIID=Label, Name=MyField, and tag "some-tag."$("*")- The set of all components on the form.$(".some-tag.some-other-tag")- All components with both tags "some-tag" and "some-other-tag"$("#MyContainer *")- All components contained in the subtree whose root has Name=MyContainer. The "MyContainer" wouldn’t be included in this set. Only its descendants.$("#MyContainer > *")- Direct children of components with Name=MyContainer. (The "MyContainer" component would not be included in this set. Only its children.)$("#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);
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:
:pressed- Selects the "pressed" style:selected- Selects the "selected" style:unselected- Selects the "unselected" style:disabled- Selects the "disabled" style:all- Selects the ALLStyles proxy for each component (so that you can target all styles together):*- Same as:all
If you want to access Style objects directly, you can use one of:
getPressedStyle()- Returns a proxy style with all the pressed styles of components in the set.getSelectedStyle()getUnselectedStyle()getDisabledStyle()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);| 1 | Finds all siblings of the source button |
| 2 | Call 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. |
| 3 | In your "mapper" callback, you will return a new Label component to replace each existing component. |
| 4 | You store the original component in the replacement’s client properties so that you can swap it back afterwards. |
| 5 | You use the "Fade" transition for the replacement. |
| 6 | In the second replaceAndWait() call’s mapper method, you return the original component which you retrieved from the replacement’s client properties. |
| 7 | You 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.

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 withGraphicsandFont, 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();
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();
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:
AUTOuses normal component inference.LEAFexposes the component but none of its descendants.GROUPexposes the component as a semantic container and retains its descendants.MERGE_DESCENDANTScombines descendant labels, descriptions, values, state, and actions into one node.EXCLUDEremoves the component but promotes its descendants to the semantic parent.EXCLUDE_SUBTREEremoves 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:
| API | Meaning |
|---|---|
| The user requests stronger foreground and background contrast. |
| The user requests labels, shapes, or patterns in addition to color. |
| Returns |
| Nonessential motion should be removed or replaced with an instant or cross-fade change. |
| Blur and translucent surfaces should use an opaque alternative. |
| The system requests a heavier text weight. |
| The operating system is transforming the displayed colors. |
| Switches should include a visible on/off distinction. |
| 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:
Use the keyboard to reach every operation. Verify visible focus, logical order, escape from modal UI, and operation without pointer gestures.
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.
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.
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.
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.

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.

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
| Target | Native representation | Notable behavior |
|---|---|---|
iOS, iPadOS, and macOS |
| Traits, values, frames, custom actions, adjustable actions, live announcements, and screen/layout transitions |
Android |
| 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 | 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:
| 1 | You 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. |
| 2 | You 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:

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:
| 1 | You loop over existing components (not newly created ones) |
| 2 | You set the desired end position not the desired starting position |
| 3 | You 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. |
| 4 | After 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-blockinganimateUnlayout(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.
| 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:
Slide - the most commonly used transition
Fast Slide - historically this provided better performance for old device types. It’s no longer recommended for newer devices
Slide Fade - the iOS default where the title area features a fade transition
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();


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. |


12.4.3. Fade and flip transitions
The fade transition is pretty trivial and accepts a time value since it has no directional context.

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 |

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();
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.

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:
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.

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.

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:
getThemeConstantisThemeConstantgetThemeImageConstant
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.
| Constant | Description/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 |
|
capsButtonTextBool |
|
capsButtonUiids | A list of the UIID’s that should be capitalized by default (in supported platforms) other than the |
defaultCommandImage | Image to give a command with no icon |
defaultNativeWindowModeBool |
|
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 |
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 |
iconUiid | Allows theming the icon component used by |
iosScrollMotionBool | Enables iOS-style scroll physics (nonlinear rubber-band during drag and critically damped tensile snap-back). When unset it defaults to |
imageviewerNavigationArrowsBool | Enables side navigation arrows by default for |
imageviewerThumbnailBarHeightMM | Sets the default thumbnail strip height (in millimeters) for |
imageviewerThumbnailsBool | Enables the bottom thumbnail strip by default for |
textUiid | Overrides the text component UIID for |
useLargerTextScaleBool | When |
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 |
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 |
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 |
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 |
infiniteMaterialImageSize | Size (in millimeters) of the generated spinner image when no custom |
infiniteDefaultColor | Hex RGB color used for the autogenerated material spinner when |
interactionDialogSpeedInt | Controls the duration in milliseconds that |
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 |
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 |
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 |
ScrollMotion | Selects the post-release flick model. |
ScrollMotionTimeConstantInt | Time constant in milliseconds for the exponential-decay flick model (active when |
scrollVisibleBool |
|
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 |
sigButtonResetUIID | Defines the UIID applied to the reset/clear button in the signature capture dialog (defaults to |
sigButtonCancelUIID | Defines the UIID applied to the cancel button in the signature capture dialog (defaults to |
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 |
textComponentAnimBool | toggles the animation mode which again can be manipulated by a method in |
textComponentFieldUIID | sets the UIID of the text field to something other than |
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 |
treeNodeImage | Picture of a file node for the |
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 |
tensileSnapMotion | Motion curve used for the tensile snap-back. |
13.4.1. Scroll motion tuning
Scroll feel is controlled by three stages, each tunable via theme constants:
While the finger is dragging past an edge--when
iosScrollMotionBoolistrue, the over-edge distance is compressed by the iOS UIScrollView rubber-band functionc * d * dim / (c * d + dim), wheredis the raw finger displacement past the edge,dimis the viewport dimension, andcisrubberBandCoefficientInt / 100(default0.55). The result is that the content feels heavier the farther you pull it, asymptotically approachingdim. WheniosScrollMotionBoolisfalse, the legacy behavior is used: a linear 1:1 follow clamped totensileLength.After the finger is lifted, while there is still velocity--the
ScrollMotionconstant selects the model.DECAY(default) uses exponential velocity decay with a time constant ofScrollMotionTimeConstantIntmilliseconds and a distance scale ofDecayMotionScaleFactorInt. Any other value (for example,FRICTION) falls back to linear friction.When the scroll comes to rest past an edge--a tensile snap-back animates the content back to the edge.
tensileSnapMotionpicks the curve:SPRINGis a critically damped envelope (iOS-like soft settle) andDECELERATIONis the legacy quadratic ease-out. The duration scales with distance but never falls belowtensileSnapMinDurationIntmilliseconds.iosScrollMotionBoolis the single switch that flips stage 1 and the default of stage 3 to the iOS-matching behavior. It defaults totrueon 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).
To make scrolling feel stiffer on iOS (less pull), reduce
rubberBandCoefficientIntfrom55toward35.To make flicks stop sooner, reduce
ScrollMotionTimeConstantInt(default500) orDecayMotionScaleFactorInt(default950).To enable iOS-style physics on Android or desktop for consistency, set
iosScrollMotionBool=trueexplicitly.To disable iOS physics on the simulator while debugging, set
iosScrollMotionBool=falsein your development theme.
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

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- aStringcontaining a number between 0-255 representing the alpha value for the background. This applies to the bgColor.margin- the margin of the component as aStringcontaining 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- anIntegerobject containing the LEFT/RIGHT/CENTER constant values defined in Component.textDecoration- anIntegervalue containing one of the TEXT_DECORATION_* constant values defined in Style.backgroundType- aByteobject containing one of the constants for the background type defined in Style under BACKGROUND_*.backgroundGradient- contains anObjectarray 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:

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:
Constant | Density | Example Device |
| ~ 88 ppi | |
| ~ 120 ppi | Android ldpi devices |
| ~ 160 ppi | iPhone 3GS, iPad, Android mdpi devices |
| ~ 240 ppi | Android hdpi devices |
| ~ 320 ppi | iPhone 4, iPad Air 2, Android xhdpi devices |
| ~ 540 ppi | iPhone 6+, Android xxhdpi devices |
| ~ 750 ppi | Android xxxhdpi devices |
| ~ 1000 ppi | |
| ~ 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

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();| 1 | This is the icon which was used instead of a logo it appears in the top right of the side menu |
| 2 | This 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. |
| 3 | The 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

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 SideCommand style is a bit more elaborate, you start with a white foreground and an opaque bluish/purple color:

You will set padding to 3 millimeters which provides everything a good feel and spacing. This is important for finger touch sensitivity.

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.

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.

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.

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.


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.

You will also update the font to a smaller size and italic styling so it will feel like a tagline.

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.

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.

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:

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.
![]() Figure 201. The header background | ![]() Figure 202. The camera icon | ![]() 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:

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.
| Port | Settings 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.
| Hint | Values | Description |
|---|---|---|
|
|
|
|
|
|
|
| Which iOS design generation the modern theme targets. Consulted
only when |
|
| Cross-platform shortcut that sets both |
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.
| Hint | Values | Description |
|---|---|---|
|
| Unset is |
|
| The native macOS port. When neither this hint nor the shared |
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: TextFieldfor a field,RaisedButtonfor the primary action,GroupBoxfor 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)andvar(--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-colorin its#Constants, and the Save button keeps its own green.



| 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.

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 readsUITraitCollection, Android readsConfiguration.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:AppsUseLightThemeon Windows,AppleInterfaceStyleon macOS and the GNOMEcolor-schemesetting 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.
| Role | Light | Dark | Used by |
|---|---|---|---|
primary |
|
| Default |
on-primary |
|
| Text printed on top of the primary fill. |
primary-container |
|
|
|
surface |
|
|
|
on-surface |
|
| Body text on surfaces ( |
surface-variant |
|
| Track of the off-state |
on-surface-variant |
|
| Secondary text ( |
surface-container |
|
|
|
surface-container-highest |
|
|
|
outline / outline-variant |
|
|
|
state-pressed |
|
| All |
state-disabled / on-disabled |
|
| All |
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.
| Role | Light | Dark | Used by |
|---|---|---|---|
accent |
|
|
|
accent-pressed |
|
| All |
accent-disabled |
|
| All |
surface |
|
| Pure surface used by |
surface-grouped |
|
| Form / ContentPane (the iOS "grouped" form bg). |
surface-tertiary |
|
|
|
text-primary |
|
|
|
text-secondary |
|
|
|
text-tertiary / text-disabled |
|
|
|
separator |
|
|
|
success |
|
|
|
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.
| Constant | Light default | Dark default | Drives |
|---|---|---|---|
|
| (see |
|
| n/a |
|
|
|
|
| All |
|
|
| All |
|
| (same) | Text color painted on top of the accent fill. |
| Constant | Light default | Dark default | Material 3 token |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
14.7.1. Override from your app’s theme.css (recommended)
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(seeCustomizing in your own themebelow). 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#bgColoretc.). 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,Titlepaint 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).MultiButtonis styled as an iOS Settings row (multi-line text ramp + chevron). Compare against Material 3’s denser list-item pattern.
Android-only behavior:
Toolbardoesn’t paint over the system status bar; the native Android status bar handles that.Tabsuse Material 3 top tabs (flat, underline-by-color). iOS modern renders Tabs as a bottom-anchored pill group via thetabPlacementIntconstant - 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):
| Constant | Default | Notes |
|---|---|---|
|
| Override to for example, |
|
| Match the empty version of whatever you chose above. |
|
| Filled circle with dot. |
|
| Empty circle. |
Set these in your #Constants { … } block; the icons rebuild on
the next style refresh.
14.10. Component-specific tuning constants
| Constant | Effect |
|---|---|
| Stretches the Switch track. Larger X = longer pill, larger Y = thicker. iOS uses 2.5 / 1.5; Material uses 3.0 / 0.9. |
| Scales the Switch thumb relative to the track. iOS 1.4, Material 1.5. |
| Pixels of breathing room between thumb and track. |
| iOS draws a faint ring around the off-state track; Material does not. |
|
|
| Distribute tabs evenly across the bar. |
|
|
| Duration of the indicator tween in milliseconds (default |
| Indicator underline thickness in millimetres (default |
|
|
| Outer diameter of the modern arc spinner in millimetres (default |
| Stroke thickness of the modern arc spinner in millimetres (default
|
|
|
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.

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.

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:
| Constant | iOS modern | Effect |
|---|---|---|
|
| Master switch for the selection drop. Off falls back to the legacy
colored underline indicator ( |
|
| The named motion preset: |
|
| Morph duration in milliseconds. Lower is snappier. |
|
| Scales the drop’s optics (magnification, chromatic aberration, accent
tint) around the preset: |
|
| Scales the settle overshoot: |
|
| 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.
| Constant | iOS 27 | Effect |
|---|---|---|
|
| Selects the measured time-based motion described above. |
|
| How much of the |
|
| Inset of the resting lens inside the pill. |
|
| Color and alpha of the resting selection platter on ports without
|
|
| The same platter in the dark appearance; defaults to the light values. |
|
| White wash of the lifted lens, which reads brighter than the bar. |
|
| Alpha of the lifted lens’s bright inner rim and dark edge ring, scaled by the lift. |
|
| Strength of the touch glow; |
|
| Peak alpha of the bright rim along the bar’s top and bottom edges; |
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): |

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.
All selectors (with some specific exceptions discussed below) are interpreted as UIIDs.
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.
Button: Defines styles for the "Button" UIID.Button.pressed: Defines styles for the "Button" UIID’s "pressed" state.Button, TextField, Form: Defines styles for theButton,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-radiusbackground(Usage below)background-colorbackground-repeatbackground-imageborder-imageborder-image-slicefont(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)colortext-aligntext-decoration(Usage below)opacitybox-shadowwidth( used for generating background-images and borders)height( used for generating background-images and borders)
15.6. Custom properties
cn1-source-dpiUsed 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 than480(2HD). If not specified, the default value will be the value of thedefaultSourceDPIInttheme constant, if specified, or480, if not specified.cn1-background-typeUsed to explicitly specify the background-type that should be used for the class.
cn1-9patchUsed to explicitly specify the slices used when generating 9-piece borders. Deprecated - Use
border-imageandborder-image-slicefor 9-piece borders.cn1-deriveUsed 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
#Constants {
--main-bg-color: red;
}
MyContainer {
background-color: var(--main-bg-color);
}#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, |
overline | Overlines text. For example, |
line-through | Strikes through text. For example, |
none | No text decoration. For example, |
cn1-3d | 3D text. For example, |
cn1-3d-lowered | 3D lowered text. For example, |
cn1-3d-shadow-north | 3D text with north shadow. For example, |
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 thecn1-round-borderandcn1-pill-borderstyles)Using the
border-radiusdirective.Using
box-shadow(Unless usingcn1-round-borderorcn1-pill-borderstyles)Using a background gradient in combination with a border or any kind
Using a different
border-width,border-style, orborder-colorfor different sides of the borderUsing 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-borderstyle. This will render a circular round border in the background natively. That is, this doesn’t require generation of an image borderThe
cn1-pill-borderstyle. This will render a pill-shaped border in the background natively. This also doesn’t require generation of an image borderThe
border-radiusproperty. This will round the corners of the border. If the style can be achieved using theRoundRectBorderin 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 colorcn1-box-shadow-inset: Set toinsetto 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).

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);
}
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-scaledcn1-image-scaled-fillcn1-image-scaled-fitcn1-image-tile-bothcn1-image-tile-valign-leftcn1-image-tile-valign-centercn1-image-tile-valign-rightcn1-image-tile-halign-topcn1-image-tile-halign-centercn1-image-tile-halign-bottomcn1-image-align-bottomcn1-image-align-leftcn1-image-align-rightcn1-image-align-centercn1-image-align-top-leftcn1-image-align-top-rightcn1-image-align-bottom-leftcn1-image-align-bottom-rightcn1-image-bordercn1-nonecn1-round-bordercn1-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 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.pnglow.pngmedium.pnghigh.pngveryhigh.png560.pnghd.png2hd.png4k.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.pnglow.pngmedium.pnghigh.pngveryhigh.png560.pnghd.png2hd.png4k.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

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:
Background Color - You can specify an RGB color to be used as the background for a component.
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.
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-scaledcn1-image-scaled-fillcn1-image-scaled-fitcn1-image-tile-bothcn1-image-tile-valign-leftcn1-image-tile-valign-centercn1-image-tile-valign-rightcn1-image-tile-halign-topcn1-image-tile-halign-centercn1-image-tile-halign-bottomcn1-image-align-bottomcn1-image-align-leftcn1-image-align-rightcn1-image-align-centercn1-image-align-top-leftcn1-image-align-top-rightcn1-image-align-bottom-leftcn1-image-align-bottom-rightcn1-image-bordercn1-nonenone
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:
native:MainThinnative:MainLightnative:MainRegularnative:MainBoldnative:MainBlacknative:ItalicThinnative:ItalicLightnative:ItalicRegularnative:ItalicBoldnative: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).
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:
platform-xxx- Target a specific platform. For example,platform-and,platform-ios,platform-mac,platform-win.density-xxx- Target a specific device density. For example,density—low,density-low,density-medium,density-high,density—high,density-hd,density-2hd, anddensity-560.device-xxx- Target a specific device type. For example,device-desktop,device-tablet,device-phone,device-tv(Apple TV / Android TV), anddevice-watch(Apple Watch / Wear OS). Thedevice-tvanddevice-watchvariants are selected at runtime whenDisplay.isTV()/Display.isWatch()returnstrue, 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).
Button {
color: #222222;
}
@media (prefers-color-scheme: dark) {
Button {
color: #f0f0f0;
background-color: #000000;
}
Button.selected {
color: #ff0000;
}
}Label {
color: black;
}
@media platform-and {
Label {
color: green;
}
}
@media platform-ios {
Label {
color: 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;
}
}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.
@media platform-and, density-high {
Label {
color: green;
}
}@media platform-ios, density-high, density-low {
Label {
color: red;
}
}@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:
Styles defined inside
@mediablocks will always take precedence over styles defined outside of@mediablocks.@mediablocks 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.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,densityin decreasing order. That is, the block that matches on platform will take precedence over the block that matches on density.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;
}
}#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-Componentincludes 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
GlassPaneor 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 invokingrepaint(), that component is drawn, and Form’spaint()method isn’t invoked. The glass pane painter is invoked in those cases and behaves as expected.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();
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…

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();
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:

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:
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:

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.
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:
Using the
GeneralPathclass for drawing arbitrary shapes.Using
Graphics.translate()to translate your drawing position by an offset.Using
Graphics.rotate()to rotate your drawing position.
There are three separate things that need to be drawn in a clock:
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.
The numbers. You will draw the clock numbers (1 through 12) in the appropriate positions.
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:
The center point of the clock.
The radius of the clock.
The clock also exposes the following parameters to help customize how it’s rendered:
The padding (that’s: the space between the edge of the component and the edge of the clock circle.
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:

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):
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.Draw number (using
drawString()) at the clock’s center. It should be rendered at the correct point due to your translation.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:

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:
Draw the hand at the clock center pointing toward
12(straight up).Translate the hand slightly down so that it overlaps the center.
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:
You will make these hands trapezoidal, and almost triangular rather than using a simple line, which means the
GeneralPathconstruction will be slightly more complex.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:

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:
Implement the
animate()method to show when the clock needs to be updated/re-drawn.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();
| 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 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 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:
All coordinates passed to the drawXXX() and fillXXX() methods will be subject to the graphics context’s transform and translation settings.
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:

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());
}
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
);
}
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.
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();
| 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();
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);
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);
}
});
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.
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;
}
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 class | Dataset & renderer | Notes |
|---|---|---|
| Draws categorical data as vertical bars. The | |
| Represents each data point as a circle whose size is proportional to a third value. | |
| Combines several XY chart types in a single plot using | |
| Smooths line series with cubic interpolation. Pass a smoothness factor to the constructor to control the curve. | |
| Renders one or more gauges on a dial, making it useful for KPI dashboards. | |
| Shows hierarchical proportions as concentric rings around a common center. | |
| Connects series of points using straight line segments. Supports optional fill areas and point markers. | |
| Splits a circle into slices that are proportional to each category value. | |
| Draws a spider/web chart that compares multiple categories across the same set of axes. | |
| Variation of | |
| Abstract base class for circular charts. Use subclasses such as | |
| Plots unconnected X/Y points with configurable marker shapes. | |
| Extends |
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
Drop your SVG next to
theme.cssinsrc/main/css/:src/main/css/ theme.css home.svgReference 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; }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 wayfont-size: 3mmbehaves elsewhere in CN1 CSS.SVGs in the wild routinely declare odd
width/heightattributes (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-dpiand 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:
It scans
src/main/css/andsrc/main/svg/for*.svgfiles. If there are none, the goal is a no-op — no registry class is emitted, no per-port stub injection happens.For each SVG it emits
target/generated-sources/svg/com/codename1/generated/svg/<Name>.java— one class per file — plus a singleSVGRegistryclass whoseinstallGlobal()registers every transcoded image with the globalResourcestable.It scans
src/main/css/*/.cssforurl(*.svg)occurrences and the enclosing rule’scn1-svg-width/cn1-svg-height/cn1-source-dpihints. Those values are baked into the registry’sinstallGlobal()call per image.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:
| Feature | Status |
|---|---|
| Full |
| Full |
| Full |
| Full |
| Full (animatable) |
| Full (shape-clipped fill on every port) |
| First-stop fallback |
| Full |
| Full |
| Full |
| Supported |
| Supported (rect/circle/path); nested clip refs ignored |
| Treated as clip — alpha masking falls back to opaque |
SVG | 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 exportSpinnerStyle {
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:
| Feature | Status |
|---|---|
Shape layers ( | Full |
Solid color layers ( | Rendered as a filled rect |
Shape fills ( | 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 |
Trim path ( | Ignored |
Gradient fills ( | 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 PNGThe transcoder ran with zero source files: the name in the CSS
url(…)doesn’t match any file undersrc/main/css/(orsrc/main/svg//src/main/lottie/for the dedicated directories). The build names each unmatched reference in a warning. A POM that doesn’t bind thetranscode-svggoal 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, callcom.codename1.ui.util.Resources.registerGeneratedImage(name, image)yourself with a freshnew 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-heightin 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
Componentwhose animation manager ticks (Labelsadded to aFormdo this automatically). For a pure-Graphics-driven capture (screenshot tests) pinAnimationTime.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
|
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
Componentthat hosts the GPU surface. You give it aRenderer.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
drawcalls.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.

18.3. Materials
A Material is a declarative description of a surface. Its Type selects the
lighting model:
| Type | Description |
|---|---|
| Flat color/texture, no lighting. Ideal for UI, emissive surfaces. |
| Diffuse (Lambert) lighting from one directional light. |
| Diffuse + specular highlight (uses |
| Unlit, for screen aligned sprites and billboards. |
| 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);
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 meshThe model below is the Khronos "BoomBox" glTF sample (a CC0 model carrying its own base-color texture), loaded this way and drawn Phong lit:

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:

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
GLSurfaceViewhosted 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 |
19.1. Concepts
| Type | Role |
|---|---|
| Static entry point: support and capability checks, permission request, session opening. |
| 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. |
| The component that renders the camera image composited with the anchored content. Add it to a form like any other component. |
| An immutable position + rotation in world space: meters, right-handed, Y up, -Z forward from the initial camera direction. Converts to a |
| A detected real-world surface (floor, table, wall) that grows and refines as the session learns the environment. |
| The intersection of a screen-point ray with real-world geometry; the bridge from a tap to a world position. |
| A tracked fixed point in the real world. The attachment point for content; also delivered for recognized images ( |
| The content placed at an anchor: a small scene graph of nodes carrying glTF models or |
| 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 anullregion.getMeshVertices()/getMeshTriangles()expose the tracked face mesh where the platform provides one, ornullwhere 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).

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
ARCapabilitiesper feature rather than assuming.Face regions: ARCore supplies
NOSE_TIP,FOREHEAD_LEFTandFOREHEAD_RIGHT; ARKit suppliesLEFT_EYEandRIGHT_EYE. Code that uses regions must handle anullregion.ARCore installation: on Android, Play Services for AR may need installing or updating; when
AR.opentriggers that flow it throws with a message saying to retry once installation completes.tvOS / watchOS: AR compiles out entirely;
AR.isSupported()reportsfalse.
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 |
20.1. Concepts
| Type | Role |
|---|---|
| The stereo component: clears the frame, splits the viewport per eye, positions the cameras from head tracking and invokes your renderer once per eye. |
| Your scene callback: |
| Which eye a frame is for: |
| Interpupillary distance (default 0.064m), per-eye field of view, clip planes and the stereo default. |
| Wires the gyroscope, accelerometer and magnetometer into an orientation with thread-safe snapshots for the render thread. |
| The deterministic sensor-fusion math behind the tracker, usable on its own. |
| Per-eye camera math over the existing |
| A 360-degree panorama viewer: an equirectangular image on the inside of a sphere with drag or gyroscope look-around. |
| Extension point feeding dynamic textures (procedural animation, future video) to |
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.

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);
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.
![]() | ![]() | ![]() |
|
|
|
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 aSpritein 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 throughGameInput. 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 flatSprite, 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 120Hz21.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, afterupdatehas 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 actionsGAME_LEFT/GAME_RIGHT/GAME_UP/GAME_DOWNonce it crosses its dead zone, soisGameKeyDown(…)keeps working.A button holds down a key code while it’s pressed, so
isKeyDown(int)andwasKeyPressed(int)report it exactly like a hardware key. Map it to a game action withDisplay.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 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)

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)

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.

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 draw21.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 draws21.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:
Subclass
GameViewand, in its constructor, create yourSpriteandAnimatedSpriteobjects and add them togetScene().Allocate GPU resources in
onSetup(GraphicsDevice)— meshes, textures and 3DModelobjects — because those need the device.Add controls (a joystick and buttons) once the view has a size.
Advance the world in
update(double dt): readgetInput(), move sprites or step aPhysicsWorld, scale all movement bydt, and triggerSoundPooleffects.Show it: add the view to a
Formand callstart().
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— anAnimatedSpritewalk cycle, a parallax backdrop and a scrollingScenecamera.Gaming3DDemoSample— lit cubeModelobjects, a textured ground and billboarded sprites under a perspective camera.GamingDemoSample— the physics tour, built on aPhysicsWorldwith a floor and walls, tap-to-drop balls that bounce (eachSpritelinked to its body) and aSoundPoolblip whose pitch varies per drop.CardGameSample— a flat 2D Memory match: a grid of cardSpriteobjects 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
updatenon-blocking — it runs on the render thread. Load assets off the render thread and hand them back withCN.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 (useSpriteSheet, which does it for you) rather than re-slicing every frame.Load every
SoundEffectup front and reuse it.Tune
setPixelsPerMeterso 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:
| Layer | Role | Who authors it |
|---|---|---|
Level data ( | A JSON description of the scene: grid, layers, placed elements + their properties, camera, lights. | The Game Builder (visual) |
Companion class ( | A thin | Generated once, then you edit the |
Gaming engine ( | 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
| Class | Purpose |
|---|---|
| The saveable level: |
| A placed thing: |
| A named band: |
| Resolve an |
| A turnkey |
| 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:gamebuilderThe 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

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
Choose a game type — scene dropdown → New scene → 2D Platformer, 3D Map or Board (or scaffold with
-Dmode).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.
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.
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.
Undo / redo every change with the toolbar arrows.
Save with Build — it writes
Level1.gameand, the first time, the companionLevel1.java.
22.5. Defining game rules
Game rules live in two complementary places.
22.5.1. Data — set in the Inspector

Select an object to edit its Transform (name, position, Layer dropdown) and its Behavior properties — the typed values that parameterize how it acts:
| Object | Behavior properties |
|---|---|
Player / Hero |
|
Coin / Gem |
|
Enemy (slime) |
|
Door / Goal |
|
Torch |
|
Card / Token / Dice |
|
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:
| Mode | Realized as | Packs | Use for |
|---|---|---|---|
| `Sprite`s in pixel space, tile grids | Platformer, Top-Down RPG | platformers, shooters, top-down adventures |
|
| Board & Card | board games, strategy, card games |
|
| 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).

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

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 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
| Goal | Purpose |
|---|---|
| Scaffold a |
| 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 byAssetCatalog.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.

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:
| Field | Meaning |
|---|---|
| The stable string key (for example, |
| The human-readable display name shown in the editor palette (for example, |
|
|
| The natural pixel size of the art (default 32×32). |
| A base ARGB colour used to render a flat placeholder until real artwork is available
(default |
| Whether a level should hold at most one of these (for example, the player / spawn point). |
| An optional reference to the source art (a resource path, an SVG/Lottie key, or the
|
| The default authoring property values copied onto a freshly placed element — a coin’s
|
23.1.1. Asset (template) versus placed element (instance)
Keep this distinction clear:
An asset (
AssetDef) is the shared template. There is exactly onecoindefinition.A placed element (
GameElement) is one instance in a level. It stores its own position, size and property values, and references the template throughGameElement#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 ofAssetDef`s (`add(def),get(id),assets()). The order is the order they appear in the palette. AGameLevelnames the pack it draws from viaGameLevel#getAssetPack().AssetCatalog— holds one or more packs and answers two questions for any asset id:def(id)returns theAssetDef(used for kind, size and default props), andimage(id)returns the artwork. Real art is supplied viasetImage(id, image); until thenimage(id)returns a cached solid-colour placeholder sized from the def and tinted with itsgetColor(), 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 id | Name | Asset ids |
|---|---|---|
| Platformer |
|
| Top-Down RPG |
|
| Board & Card |
|
| 3D Kit |
|
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—#rrggbbplaceholder colour used until real art is supplied. Optional.unique—trueto limit the level to one instance. Optional; defaultfalse.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 fromsource(a.glbis a mesh) orframeW(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 grayUnknown 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
AssetPackJSON entries alongside the built-ins so the whole pack loads throughAssetCatalog.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:
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);
}

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();
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:
| Option | Storage | File System | JAR 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.
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();

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 references | What the build adds |
|---|---|
nothing in | Nothing at all. No SQLite engine on the platforms that bundle one, no browser payload. |
| 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. |
| The cipher as well. On Android that’s |
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.legacy | Ant project | Maven project |
|---|---|---|
unset | compatibility mode | portable contract |
| portable contract | portable contract |
| 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 behaviour | Platforms |
|---|---|
| iOS |
| Simulator |
| Android, iOS |
Parameters bind as text rather than by type | iOS |
| Android, Simulator |
The parameterized methods discard statements after the first, without saying so | all |
A nested | Android |
Malformed SQL surfaces from | Android |
| Simulator |
| Simulator |
| all |
| all |
| all |
| 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 = ONafter 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.
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 methodsThe
NetworkManagererror 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();
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();| 1 | The 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. |
| 2 | This relies on the entries all being maps, which might not be the case for every API type. |
| 3 | Notice 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. |

| 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:
InputStreamdirectly fromConnectionRequest.readResponse(java.io.InputStream).XML or JSON document in the form of a
StringJSON DOM
Mapreturned 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 */
]
}
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

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
URLImagesince 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 underlyingConnectionRequestpriority 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 resultingMap, which is handy when your back end is strict about data types..cacheMode(…)and.postParameters(…)expose the same knobs asConnectionRequest, 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();
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:
| 1 | Open or create the SQLite database. The name is an identifier rather than a path. |
| 2 | Wrap it in a SQLMap, which is what maps properties to columns. |
| 3 | Declare id the primary key and let the database assign it. |
| 4 | Create 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);| 1 | Notice the use of the property’s label, which allows better encapsulation |
| 2 | You can bind picker seamlessly |
| 3 | You 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 |
| 4 | Numeric bindings "work" |

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)| 1 | The id property is useful for database storage but you want to exclude it from the UI |
| 2 | This implements the gender toggle button selection, you provide a hint to the UI so labels and values differ |
| 3 | You 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/NSBonjourServicesInfo.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:
| Platform | Injected | Notes |
|---|---|---|
Android |
| "normal" permissions; no runtime prompt |
Android API 26+ |
| Required by the OS to return a real SSID |
Android API 33+ |
| Replaces fine-location for scan flows |
iOS |
| Required since iOS 13 |
iOS |
| 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 |
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
WifiConfigurationAPI 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.WiFiSecuritymust 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 |
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
NSLocalNetworkUsageDescriptionkey explains why the app needs local-network access.The Info.plist
NSBonjourServicesarray 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 |
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.registerNetworkCallbackon API 21+, falling back to the legacyCONNECTIVITY_ACTIONbroadcast on older devices.iOS:
SCNetworkReachabilitywith 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(…)— callWiFi.disconnect(ssid)when the user navigates away.Usb.addDeviceListener(…)— callremoveDeviceListener(…).NetworkManager.addNetworkTypeListener(…)— callremoveNetworkTypeListener(…).
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)
}| 1 | Every @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
| Annotation | Purpose |
|---|---|
| Rename the JSON key. Default: the field name. |
| Omit the field from JSON. |
| Rename the XML element. Default: the field name. |
| Lift the field into an XML attribute rather
than a nested child element. Only valid for
scalar or |
| Omit the field from XML. |
26.1.2. Supported field types
| Kind | Detail |
|---|---|
Scalars |
|
|
|
| Same rules as |
| Read from the field’s generic signature. |
Nested | 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:
cn1:process-annotations(PROCESS_CLASSES phase) scanstarget/classesfor every class carrying@Mapped.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.
A
<SimpleName>Cn1Mapperclass is generated for every@Mappedtype in the same package as the source class (e.g.com.example.UserCn1Mappernext tocom.example.User). Each generated mapper exposes a publicstatic register()hook.A single
cn1app.MapperBootstrapis generated whose constructor callsUserCn1Mapper.register(),ItemCn1Mapper.register(), … for every accepted@Mappedclass.
At app start:
On iOS / Android, the build server probes the project zip for
cn1app/MapperBootstrap.class. When found it splicesnew cn1app.MapperBootstrap();into the per-build application stub right beforeDisplay.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#postInitloads the bootstrap viaClass.forName("cn1app.MapperBootstrap"). The classloader is the legitimate path here — JavaSE runs unobfuscated and the sameClass.forNamepattern 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(); }
}| 1 | JavaBeans getUser / setUser are detected automatically when the
field is private. The processor instruments setUser to fire a
change notification for two-way bindings. |
| 2 | Public field still works — direct field access falls back when no accessor matches the JavaBeans convention. |
| 3 | Explicit 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
BindAttr | Mirrors |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
| Calls |
|
|
27.2. Bind at runtime
Binding is the handle the binder returns:
| Method | Purpose |
|---|---|
| Push the current model values into every bound
component. Use after mutating the model outside the
form, or to re-sync after |
| Pull current component values back into the model. Useful before validating / submitting when none of the bindings is two-way. |
| 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:
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 callsBinders.notifyChanged(this).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.
| Annotation | Maps to | Notes |
|---|---|---|
|
| "Field must be non-empty." |
|
| Minimum string length. |
|
| Same dialect as |
|
| The standard email regex (also accepts empty — stack with |
|
|
|
|
| Bounds are inclusive; default range is unbounded. |
|
| Whitelist of allowed string values. |
|
| Escape hatch — the class must have a public no-arg constructor and implement |
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:
@Bindis applied to a field with no accessible read path — the field must be public, declare a JavaBeansgetX/isXgetter, or the annotation must name an explicitgetter.A two-way
@Bindfield on aTEXTorSELECTEDattr has no writable accessor. SettwoWay=falseto keep the binding one-way, or add a setter / explicitsetter=.@Bind(name=…)is empty.The field’s static type isn’t supported (raw collections, opaque references that aren’t
@Bindablethemselves, …).@Regexis missing a pattern, or@ExistInis 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.classand splicesnew cn1app.BinderBootstrap();into the per-build application stub beforeDisplay.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#postInitloads the bootstrap viaClass.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() { }
}| 1 | Excluded 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
| Annotation | Purpose |
|---|---|
| Marks a primary-key field. The legacy DAO uses one key;
managed sessions also support composite assigned keys.
|
| Rename the column. Default: the field name. |
| Override the SQL type. Default: inferred from the
Java type ( |
| Adds |
| 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:
@Entitylands on an interface, or an abstract entity has no@Inheritanceroot.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
mappedBydoesn’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.classand splicesnew cn1app.DaoBootstrap();into the per-build application stub beforeDisplay.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#postInitloads the bootstrap viaClass.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 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:
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.
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
| Platform | Backend |
|---|---|
iOS / macOS | AVFoundation ( |
Android |
|
JavaScript | HTML5 |
Windows | Media Foundation ( |
Linux | GStreamer ( |
Simulator |
|
TV / Watch / Car | Not supported ( |
31. Video capture constraints API
The new video capture constraints API allows you to specify "constraints" when capturing videos. Constraints include:
Video Quality (High or Low)
Maximum duration
Most file size
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
| Platform | Size | Quality | Max Length | Max 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.
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:
Chapter 33, Backend controllers, contracts and WebSockets: mapping HTTP requests to methods, sharing a typed contract with the app, and holding WebSocket connections open.
Chapter 34, Backend beans and dependency injection: splitting a server into services the build wires together, with scopes, conditions and configuration binding.
Chapter 36, Backend data access and transactions: the connection pool, the object mapping, and
@Transactionalin full, with propagation, rollback rules and savepoints.Chapter 37, Backend sessions: keeping state for a client across requests.
Chapter 38, Backend scheduling and background work: running work later, on a schedule, or on another thread.
Chapter 39, Backend tracing, metrics and management: traces, metrics, managed beans and the management endpoints.
Chapter 40, Backend MCP and the agent loop: letting an AI agent call the server’s tools, and giving an agent that builds the server a way to inspect and exercise it.
Chapter 41, Backend performance, deployment and limits: the measurements, deployment, and the limits worth knowing before choosing this.
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.javaA 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:
BackendApplicationis themain. It builds the configuration, opens the database, starts the server and installs the shutdown handler.BackendWiringis the whole of dependency injection: anewfor each bean in dependency order, the calls that set its injected fields, and its@PostConstructmethod. Chapter 34, Backend beans and dependency injection describes what goes into it.A
Routerper controller holds each route as abyte[]and binds each parameter with code written for its type.A
Cn1Aspectsclass per annotated class carries what@Transactional,@Async,@Timedand@Countedadd 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 WebSocket upgrade is matched first, against the
@WebSocketMappingroutes, and never reaches the chain.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.Then the application’s routes.
Static files from
cn1.static.rootare always last, so a file can’t shadow a route by being named like one.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=8080application-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.
| Key | What it sets |
|---|---|
| The active profile. |
| A SQLite path, or a |
| 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. |
| The port. Also read from |
| The address to listen on. Every interface by default; |
| 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”. |
| 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. |
| A directory to serve files from, after every route. |
| Whether the generated daos create their tables at start-up. True on a development profile, false everywhere else. |
| 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. |
| The listen backlog, 512 by default. |
| How long a stop waits for the requests in flight, 10 seconds by default. Zero means don’t wait. |
| Whether a TLS server offers HTTP/2 through ALPN. True by default. |
| Where static files are served ( |
| How long a request waits for a free connection before it fails, 10 seconds by default. |
| 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:
| Annotation | Reads | Binds to |
|---|---|---|
| a |
|
| a query parameter, or a field of a form body |
|
| one part of a |
|
| a request header |
|
| the body |
|
none, typed | 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 type | Response |
|---|---|
| 204 with no body, or the |
| The text, as |
A | JSON, with 200 or the |
| Exactly what the method built. |
| 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
transientones. A public field is used directly; any other through itsgetX(orisX) andsetXmethods, and is left out when it has neither.@JsonProperty("due_at")renames a field and@JsonIgnoreleaves one out, both fromcom.codename1.annotations.A
Dateis 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 as2025-03-01T09:30:00Z. Abyte[]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 rawMaporList, 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 itstoString(), 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| Key | What it sets |
|---|---|
| The origins that may call the server, separated by commas, or |
| The methods a cross-origin request may use, preflighted or not. All the common ones by default. |
| The request headers a preflight allows, |
| Response headers beyond the basic ones that the page may read. |
| Whether the browser sends cookies and the page may read the answer. A browser
refuses this with a |
| 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:
| Annotation | Use it for |
|---|---|
| Anything the application wires together. |
| A class whose |
| 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 withnew.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. Afinalfield can only be set by the constructor, and marking one@Autowiredis 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:
| Type | What it receives |
|---|---|
| The server’s configuration, for keys read at run time. |
| 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. |
| The entity manager over that pool, when the module has entities. |
| The current transaction’s persistence session, described under Chapter 36, Backend data access and transactions. |
| The current request. Only a |
| The current session. Only a |
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.
| Scope | Instances |
|---|---|
singleton, the default | One per server, built at start-up in dependency order. |
| A new instance for each injection point. Two fields of type |
| One per HTTP request, built the first time the request uses it and destroyed when the request ends. |
| 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. |
| 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:
Scheduled jobs stop starting, so none begins while the server drains.
The server stops accepting and waits, up to
cn1.server.shutdownTimeoutMillis, for the requests in flight.@Asynccalls 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.@PreDestroyanddestroyMethodrun, in reverse dependency order.The connection pool closes.
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 fragment | What to do |
|---|---|
| Annotate the implementation |
| Mark one |
| Inject one side through an |
| Take the dependency as a constructor parameter. |
| Mark the constructor the build should call. |
| A scoped or lazy bean: make the class and its methods non-final and give it a constructor with no arguments. |
| Only a |
| 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
@Beanmethod. A jar on the class path contributes nothing unless a@Beanmethod builds something from it.No proxies for aspects.
@Transactional,@Async,@Timedand@Countedrewrite the method itself, so they apply to a call throughthis, to a private method, and to an object built withnew. In Spring each of those skips the aspect, and nothing says so.No proxy around
@Configuration. One@Beanmethod 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 testA 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 binary35.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:
| Method | Builds |
|---|---|
| A request with that method. A URI template’s |
| A request with any method. |
| A |
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:
| Matcher | Checks |
|---|---|
| The status: |
| The body with |
| A value in a JSON body: |
| A response header: |
| A cookie the response sets: |
| The |
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.
webEnvironment | The application is |
|---|---|
| Reached through |
| Served on a free port of the loopback interface. |
| Served on the configured |
| 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=trueAfter 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
@MockitoBeanfield is reported as skipped. Pass-Dcn1.backend.compiledTests.strict=trueto 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
@Tagon a class or method stops the compiled run from compiling, so use one per element. A-Dtestregex 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 ignoresincludesFileandexcludesFile, and doesn’t split comma-separated or!-negated patterns inside one entry.Nested classes. The compiled run doesn’t run the tests of a
@Nestedclass. A@Nestedclass inside a@BackendTestclass isn’t supported on either run; give it a top-level@BackendTestclass 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/kotlinonly.Toolchain. The binary needs what
cn1:backend-packageneeds: 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@Autowirediscom.codename1.backend.annotations.Autowired.@BackendTestinstead of@SpringBootTest. Its options are the web environment,properties,profileandclasses.@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.
MOCKandNONEalso start the application listening on a free loopback port, because its server hosts the threads its tasks run on.The JSON path subset.
jsonPathsupports the expressions listed above, without filters or functions other thanlength().
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.
| Propagation | With a transaction open | With none open |
|---|---|---|
| Joins it. | Begins one. |
| Suspends it and begins another, on a second connection. | Begins one. |
| Sets a savepoint in it. | Begins one. |
| Joins it. | Runs without one. |
| Joins it. | Throws |
| Suspends it and runs without one. | Runs without one. |
| Throws | 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 |
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
IOExceptionfrom anything else — a file, a mail server, an outbound HTTP call — commits unless it’s listed inrollbackFor.REQUIRES_NEWneeds 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 forcn1.datasource.pool.borrowTimeoutMillisand fails.SQLite has one writer. A transaction that has written holds the database’s write lock until it ends, so a
REQUIRES_NEWmethod that writes while its caller holds the lock waitscn1.datasource.busyTimeoutMillisand fails. Reading works. On SQLite, record the audit row after the outer transaction, or in it.Transactions don’t cross threads. Work handed to
@AsyncorTasksisn’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.
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 method | What it does |
|---|---|
| Read, set and remove the session’s state. Setting |
| A copy of the attribute names. |
| A new id for the same session, with a new cookie. Call it at sign-in. |
| End the session, and ask whether it has been ended. Reading or writing an
attribute of an ended session throws |
| Whether the current request created the session. |
| How long the session may go unused before it expires, for this session alone. |
| When it was created and last used, in milliseconds since the epoch. |
| 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.
37.3. The cookie
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.
| Key | What it sets |
|---|---|
| The cookie’s name, |
| 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. |
|
|
|
|
|
|
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
synchronizedmethod 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 anExecutionException.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:
| Executor | Used by |
|---|---|
|
|
|
|
|
|
any other name |
|
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:
| Value | Runs on |
|---|---|
| A thread of the executor’s pool. |
| 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. |
| 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:
| Field | Values | Examples |
|---|---|---|
second | 0-59 |
|
minute | 0-59 |
|
hour | 0-23 |
|
day of month | 1-31, or |
|
month | 1-12, or |
|
day of week | 0-7, or |
|
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.
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 onVIRTUAL.Relying on the caller’s context. An
@Asyncbody runs outside the caller’s transaction and request. A transaction it needs, it declares itself.A cron expression read in local time. Without
zoneit’s UTC, so0 0 9 * * *fires at 09:00 UTC, which is the middle of the night somewhere.lockAtMostForshorter 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
Webcall is a client span, and sends the W3Ctraceparentandtracestateheaders so the service it reaches joins the same trace;every
Databasestatement is a client span carrying the SQL as the code wrote it, placeholders and all — the bound values are never recorded;an incoming
traceparentmakes 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:
| Variable | Property | Meaning |
|---|---|---|
|
| The collector’s base URL; |
|
| The full URL, used as it is. |
|
|
|
|
| Headers for traces only, in place of the shared ones. |
|
|
|
|
| The protocol for traces only. |
|
| Overrides the annotation’s |
|
| Extra resource attributes, |
|
|
|
|
| The ratio for the ratio samplers. |
|
|
|
|
| Spans held for export before new ones are dropped. Defaults to 2048. |
|
| Spans sent in one export. Defaults to 512. |
|
| 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.comThe 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
| Instrument | Kind | Attributes |
|---|---|---|
| histogram |
|
| gauge | |
| gauge | |
| gauge | |
| gauge | |
| gauge | (when the server has a database) |
| gauge |
|
| histogram |
|
| 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:
| Annotation | Instruments |
|---|---|
|
|
|
|
<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:
| Variable | Property | Meaning |
|---|---|---|
|
| How often to export, in milliseconds. A minute by default. |
|
| The full URL. By default the tracing endpoint’s base with |
|
| Headers for the metrics requests; by default the tracing ones. |
|
|
|
|
|
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.
| Key | What it sets |
|---|---|
| Whether the endpoints are served. On by default on a development profile, off everywhere else. |
| 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. |
| Where the endpoints live, |
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=/opsA 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.
| Key | What it sets |
|---|---|
| Whether the endpoint is served. On by default when the application has tools,
or when the development tools are available; set |
| A bearer token every call must carry. Required outside a development profile;
without it a development server listens on |
| Comma-separated browser origins allowed besides loopback ones, or |
| Where the endpoint lives, |
| 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:
| Tool | Use it to |
|---|---|
| List every route: method, path and the controller method behind it. |
| List every bean, its scope and what was injected into it, and which conditional beans are active. |
| See the active profile and every configured key, with values that look secret masked. |
| Send the server a request — |
| See the last requests served, newest first, with status and time;
|
| Read the last lines the server printed. |
| Run SQL against the server’s database. Unless |
| List the entities and their tables and columns. |
| List the scheduled jobs; start one now. |
| Read every metric’s current value, optionally only those with a prefix. |
| 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:
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
Learn the server with
backend_routes,backend_beansandbackend_schema.Change the code, restart the server, and exercise the endpoint with
backend_callbefore touching any UI. A failing endpoint is much cheaper to diagnose here than through a screen.Check what the server saw with
backend_requests, and what it stored withbackend_sql.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:
| Runtime | Requests/sec | p50 | p99 | Cold 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.
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
| Runtime | Binary or artifact | Resident 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.jdknames 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
Webcall and a TLS handshake all park it, and its host serves other connections meanwhile. SQLite, file access, resolving a host name andObject.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 withCN1_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.
wssworks 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.
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.
Open Push > Settings. Enter a name under Applications and select Create application.
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.
Open the Android or iOS provider tab under Native push providers, paste the credential JSON described below, and save it.
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:
| Value | Where to find it | Where to use it |
|---|---|---|
Application UUID | Push > Overview > Server API > Copy ID | The |
Client binding key | Push > Overview > Client binding |
|
Server API key | Account > Server API keys |
|
Select any readiness card, including a green Ready card, to open Settings on that provider’s tab.

42.1.1. Android: Firebase service-account JSON
In the Firebase project used by the installed Android app, open Project settings > Service accounts and select Generate new private key.
Open the downloaded service-account JSON file locally and copy the complete JSON object.
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.jsonto 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.

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. UsePushError.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:
| Path | Endpoint and request | Authentication and installed apps |
|---|---|---|
Keep the classic API |
| Keep the classic |
Move the server to V3 |
| Use a server API key with |
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
pushscope, 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
appIdis the selected application UUID, and check the request or credential format. For the classic endpoint,credential URL returned HTTP 404means a URL supplied inauthorcertcouldn’t be fetched. Firebaseauthcan 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-RAYheader when present, without including credentials or device tokens. The expected successful V3 response remains202with 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:
| The provider rejected the key. Delete it from your database; it will never work again. |
| The provider refused the message or the credential. Fix the send, keep the key. |
| Retried until the attempt budget ran out. |
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
deliveryIdwhen present. For classic events without it, use a key built from the provider, token or endpoint, and event timestampat. 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.
truncatedmeans 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.
eventsOmittedcounts accepted deliveries, which appear only insummary.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:
Sign the bytes you received. Any middleware that parses JSON and re-serializes it produces a different byte string that never verifies.
Compare in constant time. A comparison that returns on the first differing byte leaks enough timing to recover a valid signature.
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.

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:
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.
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.
Delivery: choose how long providers may retain an undelivered message. Console messages use safe cross-platform defaults and enter the durable queue immediately.

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()invokesCallback.registered()with aPushSubscription, then reports later token rotations through the same callback.unregister()removes the provider subscription and invokesCallback.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;PushClientmoves 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 option | iOS | Android | Simulator |
|---|---|---|---|
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.
| Capability | iOS | Android | Simulator |
|---|---|---|---|
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:
| Parameter | What to provide | Android resolution | iOS resolution |
|---|---|---|---|
| a file whose name starts with |
|
|
| image path placed in the app root | loaded from |
|
| a drawable name bundled in |
| 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.
| iOS | Android | Simulator | |
|---|---|---|---|
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()| Android | iOS | Simulator | |
|---|---|---|---|
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());
}
}
}
}| Android | iOS | Simulator | |
|---|---|---|---|
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.
| Android | iOS | |
|---|---|---|
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.
| Target | Built-in local source | Portable alternatives |
|---|---|---|
Android | Calendar Provider events | Google, Microsoft, CalDAV, and |
iOS and macOS | EventKit events and reminders | Google, Microsoft, CalDAV, and |
Codename One simulator | Isolated in-memory source | Google, Microsoft, CalDAV, and |
Win32, native macOS desktop, Linux, and JavaScript | No built-in local source; the capability set is empty | Google, Microsoft, CalDAV, and |
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
CalendarChangethroughaddChangeListener()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:

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:

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:
| Node | iOS (SwiftUI) | Android (RemoteViews) | Notes |
|---|---|---|---|
|
|
| Weight maps to |
|
|
| Child alignment via a 9-way enum |
|
|
| Android maps light/regular/medium weights to regular, semibold/bold to bold |
|
|
| Ticks natively on both |
|
|
| Native on both |
| Native | Static text | Android refreshes it on the next update only |
|
|
| Named PNG blobs, content-hash dedup |
|
|
| Value 0..1 or a state key |
|
| Falls back to linear | Android widgets lack determinate circular progress |
| Native animation | Frozen at each refresh | iOS-only nicety |
| SwiftUI | Bitmap rendered in-process | Large vector nodes cost bitmap budget on Android |
|
| Weighted | Flexible gap |
Corner radius |
| Background drawable | May render square below Android 12 |
Node action |
|
| 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);
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:

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=fetchOn 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.provisioningURLandcodename1.arg.ios.release.appext.CN1Widgets.provisioningURLLocal profile paths in
codenameone_settings.properties:codename1.ios.debug.appext.CN1Widgets.provisionandcodename1.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.
| Node | Complication | Tile |
|---|---|---|
| First two nodes only, as text and title | Rendered, but a countdown is frozen and refreshed on timeline flips |
| First node only, as a monochrome glyph the face tints | Rendered as an inline image resource |
| Becomes the ranged value | Renders natively as an arc — better than the phone widget, which degrades a circular bar to linear |
| 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
| Hint | Default | Purpose |
|---|---|---|
|
| Set to |
|
| The shared App Group id; the |
|
| Deployment target of the widget extension (the host app is unaffected) |
|
| Adds |
| 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 | |
| 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 | |
| Add | |
|
| Schedule widget timeline entry flips with exact alarms; declares |
|
| 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 |
| URL of the watch complication extension’s provisioning profile, for cloud manual-signing builds. The | |
|
| 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 |
| pinned | Override the |
|
| Set to |
|
| 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 |
|
| Wrap the Windows build in an MSIX package with a Widgets Board provider |
| package name | MSIX package identity name |
|
| MSIX identity publisher; must match the signing certificate subject |
|
| MSIX package version |
| 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.LOCKSCREENmaps to the iOSaccessoryRectangularfamily 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.msixopt-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 OneGraphicspipeline. 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 aMapViewwhen 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).

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.

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 viasetApiKey(…)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).

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:

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.

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.
}
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=huaweiThe 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"});
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 |
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:

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:
| Template | Use for | CarPlay | Android Auto |
|---|---|---|---|
| Sectioned scrolling list of rows (browse, settings) |
|
|
| Image-forward grid (categories, presets, artwork) |
|
|
| Detail pane: label/value rows + up to two actions |
|
|
| Short message + actions (errors, prompts, messaging) |
|
|
| Turn-by-turn surface with a drawable map |
|
|
| System now-playing surface for audio apps |
| 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.

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).

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:
| Category | iOS hint | Android hint | Notes |
|---|---|---|---|
Audio |
| (media browser service) | Default CarPlay entitlement when none specified |
Communication |
|
| Read-aloud / voice reply |
Navigation |
|
| Adds the drawable map surface + |
Point of interest |
|
| 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
CarApplicationin 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.

Selecting a browsable row pushes the next screen, just as it would in the car — here a CarGridTemplate, reachable via the Back button:

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;
CarNowPlayingTemplateroutes 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.
| Constant | Reading |
|---|---|
| Acceleration including gravity, in meters per second squared. |
| Acceleration with gravity removed, in meters per second squared. |
| The gravity vector on its own, in meters per second squared. |
| Rate of rotation around each axis, in radians per second. |
| The ambient magnetic field, in microtesla. |
| 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_DOWNandTYPE_FLIP_FACE_UP— the device was turned screen down or screen up.TYPE_TILT_LEFT,TYPE_TILT_RIGHT,TYPE_TILT_FORWARDandTYPE_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 gesturesA 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 hintios.NSMotionUsageDescriptionwith 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:
| Factory | Use for |
|---|---|
| URI records — launches the associated app on tap |
| Human-readable text |
| Binary payloads (vCard, JSON, …) |
| Custom vendor types |
| 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. |
| Accessor | Android | iOS | Notes |
|---|---|---|---|
| yes | yes | ISO 7816 — EMV, ePassport, smart cards |
| yes | — | Apple doesn’t expose MIFARE Classic on iOS |
| yes | yes | NTAG21x, Ultralight C |
| yes | yes (limited) | Raw ISO 14443-3A |
| yes | — | Raw ISO 14443-3B (Android-only) |
| yes | yes | FeliCa (Suica / PASMO / ICOCA) |
| 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=otherThe 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.
| Reference | Android injected | iOS injected |
|---|---|---|
|
|
|
|
|
|
Override any of the defaults with the matching build hint:
| Build hint | Default | Notes |
|---|---|---|
| "Hold near an NFC tag to continue" | Localise this — Apple rejects builds that ship the default copy |
|
| Multi-line entitlement value |
| (none) | Comma-separated; required for HCE on Android |
|
|
|
|
| Force lock screen unlock before APDU routing |
| falls back to | 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 item | Effect |
|---|---|
Hardware Available / NFC Enabled / HCE Available | Toggle what |
Next read outcome | Pick |
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 |
Send APDU to HCE service… | Hex-entry dialog that dispatches the bytes to your registered |
Deactivate HCE field | Fires |
All toggles persist between simulator runs (under NfcSim.* user preferences) so you can drive tests deterministically.
49.7. Platform behaviour
| Platform | NDEF read | NDEF write | Tag tech | HCE | Notes |
|---|---|---|---|---|---|
Android | yes | yes | full set | yes |
|
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 |
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.
| Capability | Android | iOS / macOS | Simulator (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 usage | Android injected (target SDK 31+) | iOS injected |
|---|---|---|
Any |
|
|
Scanning ( |
| — |
Connections (GATT, |
| — |
Peripheral mode ( |
| — |
Classic ( |
| 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 hint | Default | Notes |
|---|---|---|
|
| Declares that scan results never derive the user’s location, which caps |
|
| Marks the BLE hardware feature required so stores hide the app on devices without it |
| (none) |
|
| "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 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 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"):
| Hook | Menu label | Effect |
|---|---|---|
| Toggle Adapter | Flips the simulated adapter between |
| Add Demo Peripheral | Registers the canonical demo device: |
| Push Demo Notification | Pushes a rolling one-byte notification from the demo peripheral’s |
| Disconnect All | Drops every live connection from the remote side |
| Clear Peripherals | Removes every registered virtual peripheral |
| Use Simulator Backend | Switches back to the virtual stack |
| Use Native Backend | Switches to the host machine’s real radio |
| (API only) | Arms the next GATT read to fail with |
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
| Platform | Address semantics | MTU | Bonding | Notes |
|---|---|---|---|---|
Android | Real MAC address |
|
| 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; | OS-managed, triggered by encrypted characteristics; | Background operation needs |
Simulator (mock) | Synthetic ( | 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 |
JavaScript | Per-origin Web Bluetooth identifier | Fixed at 512 | Browser-managed | One user-picked device per chooser; |
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.
| Capability | iOS | Android | Simulator and desktop | JavaScript |
|---|---|---|---|---|
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.
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
| Hint | Default | What it does |
|---|---|---|
| derived from the package name | Comma-separated list of the service ids the app passes to |
| unset | Comma-separated Bluetooth service UUIDs the association picker may discover. Required for the picker to find anything on iOS. |
|
| Requests the |
|
| Declares the watch companion profile permission, for an app that associates with |
|
| The same for |
|
| The same for |
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.
| Capability | iOS | Android | Simulator and desktop | JavaScript |
|---|---|---|---|---|
System call UI | yes (CallKit) | yes (Android 8 and later, see below) | simulated | — |
Ring while the app isn’t running | yes ( | 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.
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.
CallStateRINGING,DIALING,ACTIVE,HELD,ENDED. Incoming calls start atRINGINGand outgoing ones atDIALING, butACTIVEandHELDcycle in both directions —CallSession.setHeld(false)and a fulfilled resume both put a held call back toACTIVE, so code that treatsHELDas one-way rejects a legitimate resume or leaves media paused.ENDEDis 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 withCallError.INVALID_IDonce a call has ended, which is how a bookkeeping bug in the app surfaces; the reporting methods that returnvoid, such asreportConnectedandupdate, return without a word instead, so don’t wait on them for that signal.CallDirectionINCOMINGorOUTGOING, 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 fromCalls.getSessions()— rather than keeping your own note of the direction beside the call id.CallEndReasonREMOTE_ENDED,LOCAL_ENDED,UNANSWERED,BUSY,FAILED,FILTERED.FILTEREDcovers 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 fromUNANSWEREDin that the call never rang. The Android port also reports it for a fulfilled reject, though, so a Decline from the lock screen arrives asFILTEREDthere while a hang-up after answering arrives asLOCAL_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.CallAvailabilitywhat
Calls.getAvailability()answers before you report a call, andAVAILABLEis 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 answerNOT_PERMITTEDapplies to incoming calls alone — an outgoing call never rings, soreportOutgoinggoes through and refusing it would take away a call the user started. The values need three different responses, not two.EMERGENCY_CALL_IN_PROGRESSandOTHER_APP_IN_CALLpass on their own, so a retry is the right move —THIS_APP_IN_CALLis in the same family, though Android’s bridge never reports it by design, because Telecom accepts a second call from the same account.NOT_CONFIGUREDmeansCalls.configurehasn’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 aconfigurethat threw answeredUNAUTHORIZEDand left the port without a registered account, so calling it again without fixing what it reported changes nothing.NOT_PERMITTEDis Android only — iOS never answers it — and it covers three conditions with three different fixes, none of which is the calling switch.MANAGE_OWN_CALLSmissing: that one is a normal permission granted at install, so there’s no prompt behind it andCalls.requestPermissionsdoesn’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_NOTIFICATIONSnot yet granted on Android 13 and later: that one is a runtime permission, asking forPERMISSION_NOTIFICATIONSis 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. OnlyUNSUPPORTEDis permanent.CallHandleandCallHandleTypewho the call is with, and one of
GENERIC,PHONE_NUMBERorEMAIL_ADDRESS. Build a handle withCallHandle.phone(…),CallHandle.email(…)orCallHandle.generic(…)rather than by hand; the type travels with the value and decides how the system displays and dials it.CallIdstatic helpers for the identifier both ends of a call have to agree on, which is a
Stringrather than a type of its own.CallId.random()mints one,CallId.isValid(…)checks one that arrived over your own connection, andCallId.normalize(…)puts it in the form the platform expects.CallErrorandCallExceptionwhy 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
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
| Hint | Default | What it does |
|---|---|---|
| the app’s display name | The name shown above a call in the system UI. |
| the system ringtone | A sound file in the bundle to ring with. |
| none | A template image shown beside the call. |
| none | The name shown when a push carries no |
|
| Seconds a pushed call waits for your code before the system ends it as unanswered. |
|
| Whether the provider offers video calls. |
|
| Whether calls appear in the system call log. |
|
| The text on the Android ringing notification. |
|
| The Android answer button. |
|
| The Android decline button. |
|
| The notification channel the user sees in Settings. |
|
| Whether video calls are offered, on both platforms. Overridden per platform by |
|
| Whether the Android manifest declares |
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.
| Capability | iOS | Android | Simulator and desktop | JavaScript |
|---|---|---|---|---|
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.
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 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 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. |
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
| Hint | Default | What it does |
|---|---|---|
| injected as | The Personal VPN capability. Enable it on the App ID or the app won’t sign. |
|
| Generates the packet tunnel extension. Also your assertion that the App ID holds the Network Extension grant; see above. |
| none | Which |
| none | The extension’s own provisioning profile, base64, for a device archive. |
| 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.
| Capability | iOS 26+ | iOS < 26 | watchOS | Android | Simulator | Desktop / 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
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:
requestAuthorization(…)resolvingtruemeans 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.getReadAuthorizationStatus(…)returnsUNKNOWNon 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.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.HKWorkoutSessionexists on watchOS and on iOS 26 and later, andandroidx.health.servicesexists 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.
| Hint | Effect |
|---|---|
| Why your app reads health data. Required if you touch the store; the build fails without it. |
| Why your app writes health data. Required if you write. |
| Adds the background-delivery entitlement. Inferred from observer usage. |
| Adds the estimate-recalibration entitlement. |
| Adds |
| Hint | Effect |
|---|---|
| Comma-separated data types to read. Required. |
| Comma-separated data types to write. |
| Adds |
| Adds |
| Your privacy policy. Required. |
| 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").
| Action | Effect |
|---|---|
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 |
| 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.
| Capability | iOS / iPadOS | watchOS / tvOS | Android (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=trueEverything 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.homekitentitlement is added only when your app actually touches accessories. An app that only callsgetAvailability()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.commissioningis 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.JAMMEDis 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 –
UIPrintInteractionControllerpresents the AirPrint sheet; on the iPad it appears as a popover. The completion handler reports completed, cancelled or failed.Android – the
android.printframework. PDFs stream through aPrintDocumentAdapter; images go through the support libraryPrintHelper, 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.PrinterJobwith 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.PdfAPI 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.
57. Deep-Link Routing
Codename One can dispatch a deep link — an iOS Universal Link, an Android
App Link, a custom-scheme URL, a push-notification payload, or anything
else the platform delivers as a URL — to a specific Form by class or by
static factory method. The same route table powers in-app navigation
through Navigation.navigate("/path"), so the application chooses
declarative or imperative per call site without maintaining two parallel
maps.
The application surface is two annotations plus a tiny Navigation
class — five static methods and one value type.
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
| Pattern | Matches | Bound |
|---|---|---|
|
| — |
|
|
|
|
|
|
|
|
|
Query string parameters are bound the same way as path variables. The build prefers a path-variable match for a given name and falls back to the query string when the pattern doesn’t include that name.
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:
@Routedeclared on a class that doesn’t extendFormPattern with no leading
/or empty valuePath variable with no matching
@RouteParamon the constructor / factoryDuplicate pattern declared on two different targets
@Routeon an abstract class or non-public staticmethod
57.5. iOS Universal Links
Host an apple-app-site-association JSON file at
https://your.domain/.well-known/apple-app-site-association over HTTPS
without redirects. The plugin’s AasaBuilder produces the payload:
{
"applinks": {
"details": [
{
"appIDs": ["ABCDE12345.com.example.app"],
"components": [
{ "/": "/users/*" },
{ "/": "/promo/*" }
]
}
]
}
}Tell iOS which domains your app claims by setting the
ios.associatedDomains build hint — a comma-separated list of
applinks: entries. The iOS builder writes the Associated Domains
entitlement so the resulting .ipa is signed for those domains
automatically.
codename1.arg.ios.associatedDomains=applinks:example.com,applinks:www.example.com57.6. Android App Links
Host an assetlinks.json file at
https://your.domain/.well-known/assetlinks.json. The plugin’s
AssetLinksBuilder produces the payload:
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.app",
"sha256_cert_fingerprints": ["14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:16:A0:83:42:E6:1D:BE:A8:8A:04:96:B2:3F:CF:44:E5", "7B:5A:1F:C9:0E:52:44:7C:82:19:6E:38:0A:B7:D4:11:5C:63:9F:28:E1:47:8B:30:A6:C2:5D:94:F0:6B:38:19"]
}
}
]Tell Android which URLs to intercept by setting the
android.xintent_filter build hint. The Android builder injects the
value into the manifest verbatim, so it’s the whole <intent-filter>
element, with \n for the line breaks:
codename1.arg.android.xintent_filter=<intent-filter android:autoVerify="true">\n <action android:name="android.intent.action.VIEW"/>\n <category android:name="android.intent.category.DEFAULT"/>\n <category android:name="android.intent.category.BROWSABLE"/>\n <data android:scheme="https" android:host="your.domain" android:pathPrefix="/users"/>\n</intent-filter>Three parts of that carry weight. android:autoVerify="true" is what makes
Android fetch the assetlinks.json above at install time and route the link
straight into your app. Leaving it out doesn’t fall back to an app chooser:
from Android 12 onward an unverified web link goes to the default browser, and
your filter never sees it until the user turns the association on by hand under
Settings > Apps > Open by default. The <data> element needs
android:scheme="https" and the host, and narrowing it with
android:pathPrefix keeps the app from claiming every URL on the domain. And
the hint holds one value, so an app that also registers a custom scheme puts
both <intent-filter> elements in the same string rather than setting the hint
twice.
The SHA-256 fingerprint comes from keytool -list -v -keystore …, or
from the Play Console under Setup > App integrity when using Play
App Signing.
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.app57.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.
| Capability | iOS and macOS | Android | Simulator and desktop | JavaScript |
|---|---|---|---|---|
Restore after the process is killed | yes | yes | yes | yes |
Restore the | 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 ( | — | simulated | — |
Carry state to any other device | your | your | your | your |
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
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
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
@Routeproduces. An app that assumedrestore()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
| Hint | Default | What it does |
|---|---|---|
| unset | Whether this project wants the iCloud key-value store. Left unset the build decides from the bytecode. Set |
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=falseWith 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. |
59.5. Publishing content to device search
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 |
59.7. What each platform actually does
| Capability | iOS | Android | Simulator |
|---|---|---|---|
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:
| Hint | Effect |
|---|---|
| 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 |
|
|
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 |
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 |
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.myappios.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.0displayName 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.mobileprovisionThe 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:
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.The one step that genuinely needs the issuer’s backend - producing
encryptedPassDatafrom 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-provisioningentitlement 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/provisionThat 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/loginIn 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.mobileprovisionAlternatively, 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.plistwith theNSExtensiondictionaryMyWalletExtension.entitlementsOptional
buildSettings.propertieswith Xcode build settings, one per line (setIPHONEOS_DEPLOYMENT_TARGET=14.0for Wallet extensions)The extension’s own
.mobileprovisionfor 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:
Place the
.mobileprovisionfile insideios/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.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.mobileprovisionThe unqualified
codename1.ios.appext.MyWalletExtension.provisionis the fallback for both build types.Host it and name the URL in
ios.appext.MyWalletExtension.provisioningURL(or theios.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/WalletUIExtensiontargets - open the generated Xcode project and build.WalletExtension.isSupported()returnsfalsein 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 -
sendSMSwon’t show a UI and will send in the backgroundSMS_INTERACTIVE -
sendSMSwill show an SMS sending UISMS_BOTH -
sendSMScan 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 from0.82×(Extra Small) through1.00×(Large, default) up to1.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 as3.12×. These values match what every native iOS app sees fromUIFontMetrics.Android: the scale is
Configuration.fontScale, which is bucketed by Android (typically0.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!

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 |

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();
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();
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. |

"CFBundleDisplayName"="Hello App";"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 |
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 codesThese 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.
"CFBundleDisplayName"="Hello App";
"NSCameraUsageDescription"="This app needs to use your camera to scan bar codes";"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 Englishcn1_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.pngat 36, 48, 72, 96, 128, 144 and 192 pixels for legacy launchers.When
android.enableAdaptiveIcons=truethe same variants are also written asmipmap-<lang>[-r<COUNTRY>]-<dpi>/ic_launcher.pngandic_launcher_foreground.png. The default adaptive background and themipmap-anydpi-v26/ic_launcher.xmldefinition 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>.appiconsetinsideImages.xcassetscontaining the 120×120, 180×180, 152×152 and 167×167 (iPad Pro) PNGs along with a matchingContents.json.The build adds the matching
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMESXcode build setting soactoolemits a coherent partialInfo.plistcovering bothCFBundlePrimaryIconandCFBundleAlternateIcons. The user’sInfo.plistis left untouched; injectingCFBundleIconsmanually would be dropped during the actool merge and is therefore avoided.The
CodenameOne_GLAppDelegateis 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.afterFinishLaunchinghook, 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.pngis treated the same ascn1_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_*.pngfiles are removed after processing so they aren’t shipped as stray bundle resources.To override the default (non-localized) icon, keep using the
Iconsetting incodenameone_settings.propertiesor theiconimage inCN1Resource.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 useClass.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=falsetrue 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();
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();
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);
}
}
});
});62.8. Gallery
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:
Subclass a
Componentoverridepaint, implement event callbacks etc.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.

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.

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:
Boolean.TRUE- the URL can be executedBoolean.FALSE- the URL isn’t supported or the app is missingnull- 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.SpeechRecognizerandcom.codename1.media.TextToSpeech— on-device speech-to-text and speech synthesis.com.codename1.security.SecureStoragenon-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.tflitemodels.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 string63.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:
| Value | Behavior |
|---|---|
| The default on a device. Calls go to the configured provider, even in the simulator. |
| The default in the simulator. If Ollama is reachable, route
OpenAI calls through it. Otherwise behave like |
| 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 rendered in the JavaSE simulator under the iOS Modern themeThe 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:
| UIID | Applies to |
|---|---|
| The outer container. |
| The scrollable message column. |
| The container for a user message. |
| The container for an assistant message. |
| The container for a system message. |
| The inner |
| The animated typing dots. |
| The input strip. |
| The text field inside the strip. |
| The send button. |
| The attach button. |
| 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:
| API | Android | Apple default / optional backend |
|---|---|---|
| ML Kit text recognition | Vision / |
| ML Kit bundle for that script | Vision recognition languages / |
| ML Kit barcode scanning | Vision / |
| ML Kit face detection | Vision / |
| ML Kit image labeling | Vision / |
| ML Kit pose detection | Vision / |
| ML Kit selfie segmentation | Vision / |
| 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.
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 hint | Default | Description |
|---|---|---|
| (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.
| Policy | What it means |
|---|---|
| The password is required every launch, and every page load in a browser. Nothing that can reopen the vault is written to the 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. |
| 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:
| Browser | Device key | Passkey PRF | storage.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
| Threat | Protected |
|---|---|
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 |
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. |
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. |
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 (subinaries, 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 addandroid.playIntegrity.verifyUrl=https://backend.example.com/integrityto also attest at launch on a background thread and exit if your backend rejects the token.android.playIntegrityVersionoverrides the bundled SDK version.ios.appAttest=true— enables App Attest (DeviceCheck.framework) and the runtime token API.ios.appAttest.environmentisdevelopment(debug builds) orproduction(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;warnonly 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.reportobserves without changing behavior, which is the safe way to measure how often this happens in your user base first.strictalso 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 callsetHideOverlayWindows(). Defaults totrue.
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_WINDOWSmanifest permission. Without it the OS throws, so the call is skipped and logged instead.android.tapjackingGuard=truedeclares it for you; if you drive the runtime API without that hint, addandroid.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.securityandjavax.crypto.iOS routes through Apple’s CommonCrypto and the Security framework (via
Ports/iOSPort/nativeSources/CN1Crypto.{h,m}and the correspondingIOSImplementationoverrides).
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 DERA 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 |
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
urito your backend over HTTPS and have it return a PNG. The Google Charts API endpointhttps://chart.googleapis.com/chart?cht=qr&chs=300x300&chl={url-encoded-uri}and the open-sourcehttps://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 withURLImageorEncodedImage.create(bytes).Use a QR cn1lib. Community cn1libs such as
QRMakerprovide 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=inuri) 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.
| Class | Constant | Algorithm string |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| Same as the JWT |
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. |
65.11.11. Key sizes and recommended defaults
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.
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.
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.
The app sends that statement to the attestation service, together with any runtime signals it has collected.
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.
The app sends that token to your API in the
X-CN1-Attestheader, 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 hint | Default | Description |
|---|---|---|
|
| 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. |
| (service default) | The policy generation this build was compiled against. |
| (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. |
| (none) | Pins the key that signs the tokens, so a compromised DNS answer can’t introduce a signing key the app will trust. |
| (Codename One cloud) | Override for a private deployment of the verification service. |
|
| The hardening engine’s own failure policy: |
|
| 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:
| Policy | Token | Pins | When no token can be obtained |
|---|---|---|---|
| no | no | Not applicable. This is what an unregistered host gets. |
| yes | yes | Fail open. The request goes out without a token and your backend decides. Right for most hosts. |
| yes | yes | Fail closed. |
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
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. NoteonRedirect()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, whichConnectionRequestroutes 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.
| Status | Transient | Meaning |
|---|---|---|
| - | A valid token is in hand. |
| no | This build has no attestation engine. Nothing is enforced. |
| no |
|
| yes | The service couldn’t be reached. |
| yes | The service answered with an error. |
| yes | This device is being throttled. Retry after a backoff. |
| no | The service evaluated this device and declined to mint a token. |
| 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:
| Condition | Behaviour |
|---|---|
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. |
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.
| Transport | Coverage |
|---|---|
| Token and pinning, automatically. |
| Token via |
| The initial navigation only, through |
A native or third-party HTTP client | Nothing automatic. Use |
// 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:
pkgandplat— the package name and platform the token was minted for. Check them against the app you expect.vrd— the verdict:attested,monitoredorunverified.dec— the decision under the current policy:allow, orwould_denywhile 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 item | What it makes reachable |
|---|---|
Attestation Supported | Unchecking it simulates a platform with no attestation at all. |
Attestation Result |
|
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 |
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()returnsfalse.fetchToken()always completes, withUNPROTECTED, 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 realDeviceIntegritydata, 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.
| Transform | Ports | Notes |
|---|---|---|
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 |
String constant encryption | iOS, Android, Windows, Linux, desktop | Both channels are handled: the 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 ( 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 Another exclusion: string concatenation such as |
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 hint | Default | Description |
|---|---|---|
|
| Master switch: |
| (level) | Override renaming on/off independently of the level. |
| (level) |
|
| (level) | Override control-flow obfuscation on/off. |
| (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 |
|
| Per-port opt-out ( |
|
| 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. |
|
| Escape hatch: allow a local or source-project target to build unhardened instead of failing the pre-flight. |
| (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.
off | standard | aggressive | paranoid | |
|---|---|---|---|---|
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
| Platform | Implementation | Notes |
|---|---|---|
iOS |
| Add the |
Android API 29+ |
| Face / iris / fingerprint per |
Android API 23-28 |
| Fingerprint only. Goes through the support library rather than |
JavaSE simulator |
| Toggle hardware availability, per-modality enrolment, and the next-call outcome. |
All other ports | Non-supporting fallback |
|
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.frameworkis linked into the generated Xcode project.Android:
<uses-permission android:name="android.permission.USE_BIOMETRIC" />(andUSE_FINGERPRINTwithmaxSdkVersion=28for the legacy path) is injected intoAndroidManifest.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 accountThe 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.appSecureStorage.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:
| Code | Meaning |
|---|---|
| No biometric hardware or the platform doesn’t support it |
| Hardware present but the user hasn’t enrolled |
| Too many failed attempts; temporary |
| User must unlock with passcode before biometrics can be used again |
| Device has no PIN / pattern configured (can’t use biometrics) |
| User dismissed the prompt |
| OS dismissed the prompt (app backgrounded, system pre-empted) |
| Prompt completed but the user wasn’t recognized |
|
|
| 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 useSecureStorage.
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()andcanAuthenticate().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:ASWebAuthenticationSessionon iOS and Android Custom Tabs. Elsewhere it falls back toBrowserWindow, and what that means differs: the Web port opens a real top-level browser window, while JavaSE renders a JavaFXWebViewinside 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 awindow.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 anOidcClientyou configured ahead of time instead ofdiscover(). 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 andauthorize()simply never completes. That applies to Apple’sform_postbridge 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.
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
GoogleSignInSDK 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_postresponse_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:
Fetches
accounts.google.com/.well-known/openid-configuration.Generates a PKCE pair (S256), a
state, and anonce.Opens the OS system-browser sheet at the discovered authorization endpoint.
Exchanges the code for tokens on the discovered token endpoint.
Verifies
stateandnonce, decodes the ID token, and persists the tokens via the defaultTokenStore.
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
In the Apple Developer portal, create a Services ID alongside your App ID. Enable "Sign in with Apple" for both.
Configure the Services ID’s Web Authentication Configuration with the primary App ID and your redirect URI.
Generate an AuthKey private key in the Apple developer portal (
.p8file). Apple doesn’t allow public mobile clients to mint theclient_secretJWT 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 viasignIn(…)doesn’t require the native SDK at all.On iOS, the legacy code uses
GIDSignIn(Google Sign-In SDK). The new code usesASWebAuthenticationSessionviaSystemBrowser, 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







