<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Shai Almog</title>
    <description>The latest articles on DEV Community by Shai Almog (@codenameone).</description>
    <link>https://dev.to/codenameone</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F417973%2Fa7b22dd9-5565-48f5-bfab-7e5035b3888f.png</url>
      <title>DEV Community: Shai Almog</title>
      <link>https://dev.to/codenameone</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/codenameone"/>
    <language>en</language>
    <item>
      <title>VideoIO, PCM Mixing And Timed Whisper Captions</title>
      <dc:creator>Shai Almog</dc:creator>
      <pubDate>Wed, 15 Jul 2026 14:58:28 +0000</pubDate>
      <link>https://dev.to/codenameone/videoio-pcm-mixing-and-timed-whisper-captions-1l3n</link>
      <guid>https://dev.to/codenameone/videoio-pcm-mixing-and-timed-whisper-captions-1l3n</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foowfh4zo0imdhk1ra7r8.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foowfh4zo0imdhk1ra7r8.jpg" alt="VideoIO, PCM Mixing And Timed Whisper Captions" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This release turns media from "play a file" into "build a file."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is Codename One?&lt;/strong&gt; Codename One is an open-source framework for building native iOS, Android, desktop, and web apps from a single Java or Kotlin codebase. Learn more at &lt;a href="https://www.codenameone.com/" rel="noopener noreferrer"&gt;codenameone.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/codenameone/CodenameOne/pull/5315" rel="noopener noreferrer"&gt;PR #5315&lt;/a&gt; adds &lt;code&gt;VideoIO&lt;/code&gt;, a cross-platform video subsystem that can encode app-rendered frames and audio into a standard video file, then decode an existing clip back into exact RGBA frames and PCM audio. &lt;a href="https://github.com/codenameone/CodenameOne/pull/5317" rel="noopener noreferrer"&gt;PR #5317&lt;/a&gt; adds &lt;code&gt;AudioMixer&lt;/code&gt;, a sample-accurate PCM timeline. &lt;a href="https://github.com/codenameone/CodenameOne/pull/5319" rel="noopener noreferrer"&gt;PR #5319&lt;/a&gt; adds timed Whisper transcription segments with SRT and VTT output.&lt;/p&gt;

&lt;p&gt;Together, these APIs let Codename One generate proper videos across supported app platforms.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F15w3r985kye1kn5y28lv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F15w3r985kye1kn5y28lv.png" alt="Frame-accurate VideoIO decode output from the Codename One test app" width="800" height="1736"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Encoding App-Rendered Frames
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;VideoIO&lt;/code&gt; is gated like other optional platform APIs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(!&lt;/span&gt;&lt;span class="nc"&gt;VideoIO&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isSupported&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you build a writer. Frames are ordinary &lt;code&gt;Image&lt;/code&gt; instances, so the pixels can come from your UI, a game renderer, an animation scrubber, a generated chart, or a custom drawing routine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FileSystemStorage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getAppHomePath&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"/demo.mp4"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;VideoWriter&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;VideoWriterBuilder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;container&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;VideoIO&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;CONTAINER_MP4&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;720&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;frameRate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;videoCodec&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;VideoIO&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;CODEC_H264&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;videoBitRate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4000000&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasAudio&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;audioCodec&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;VideoIO&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;CODEC_AAC&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sampleRate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;44100&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;audioChannels&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Image&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;renderFrame&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeFrame&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000L&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;30L&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kt"&gt;short&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;pcm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;renderAudioTrack&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeAudio&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pcm&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;44100&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;close&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact codecs available are platform-dependent. &lt;code&gt;VideoIO.getAvailableEncoders()&lt;/code&gt; and &lt;code&gt;getAvailableDecoders()&lt;/code&gt; expose what the device actually supports, including hardware acceleration flags.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frame-Accurate Decode
&lt;/h2&gt;

&lt;p&gt;The old &lt;code&gt;Media&lt;/code&gt; API is a player. It can seek, but playback seek is usually key-frame based. That is not enough when you are generating thumbnails, verifying a rendered animation, extracting frames for processing, or round-tripping a generated video in a test.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;VideoReader.frameAt(ms)&lt;/code&gt; returns the exact frame at a timestamp:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;VideoReader&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;VideoIO&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getVideoIO&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;openReader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;VideoFrame&lt;/span&gt; &lt;span class="n"&gt;oneSecond&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;frameAt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="nc"&gt;Image&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;oneSecond&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getImage&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;close&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also resample a whole clip to a constant frame rate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readFrames&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getImage&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTimestampMillis&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And when the backend supports it, &lt;code&gt;readAudio()&lt;/code&gt; returns the audio track as an &lt;code&gt;AudioBuffer&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;AudioBuffer&lt;/span&gt; &lt;span class="n"&gt;audio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readAudio&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;JavaScript currently supports frame decode and full encode, but decoded audio extraction is not wired there yet. TV, Watch and Car targets do not expose &lt;code&gt;VideoIO&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  PCM Mixing On One Clock
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;AudioMixer&lt;/code&gt; is intentionally small. It combines interleaved float PCM tracks in the normalized &lt;code&gt;[-1, 1]&lt;/code&gt; range on a single sample clock.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;AudioBuffer&lt;/span&gt; &lt;span class="n"&gt;music&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AudioBuffer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;musicPcm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;music&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;copyFrom&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;44100&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;musicPcm&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="nc"&gt;AudioBuffer&lt;/span&gt; &lt;span class="n"&gt;voice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AudioBuffer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;voicePcm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;voice&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;copyFrom&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;44100&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;voicePcm&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="nc"&gt;AudioMixer&lt;/span&gt; &lt;span class="n"&gt;mixer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AudioMixer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;44100&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;mixer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addTrack&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;music&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.65f&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;mixer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addTrack&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;voice&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0f&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="nc"&gt;AudioBuffer&lt;/span&gt; &lt;span class="n"&gt;mixed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mixer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;mix&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you already know exact sample-frame offsets, use &lt;code&gt;addTrackAtFrame(...)&lt;/code&gt; instead of millisecond offsets. The output clips to &lt;code&gt;[-1, 1]&lt;/code&gt; and can be written through &lt;code&gt;WAVWriter&lt;/code&gt; or passed into a &lt;code&gt;VideoWriter&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;pcm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;mixed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSize&lt;/span&gt;&lt;span class="o"&gt;()];&lt;/span&gt;
&lt;span class="n"&gt;mixed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;copyTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pcm&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="nc"&gt;WAVWriter&lt;/span&gt; &lt;span class="n"&gt;wav&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WAVWriter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"mix.wav"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;mixed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSampleRate&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;mixed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getNumChannels&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
        &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;wav&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;write&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pcm&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mixed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSize&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;wav&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;close&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Subtitles From Timed Whisper
&lt;/h2&gt;

&lt;p&gt;The Whisper cn1lib already returned text. The new transcription API returns timed segments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;modelPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FileSystemStorage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getAppHomePath&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"/ggml-base.en.bin"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="nc"&gt;Transcriber&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;WhisperRecognizer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;transcriber&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modelPath&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;TranscriptionResult&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;transcribe&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TranscriptionRequest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audioPath&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;setLanguageTag&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"en-US"&lt;/span&gt;&lt;span class="o"&gt;)).&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;srt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toSrt&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;vtt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toVtt&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;get()&lt;/code&gt; call is blocking, so run it off the EDT or use the returned &lt;code&gt;AsyncResource&lt;/code&gt; callback. Those segment timestamps are the missing link between "we transcribed the video" and "the video has subtitles." A generated training clip, game replay, tutorial or support recording can now get caption files automatically.&lt;/p&gt;

&lt;p&gt;The Whisper PR also fixed native packaging gaps: Android now ships a JNI AAR with native slices, iOS exposes timed segments, JavaSE keeps a fallback segment, Linux and Windows native bridge modules are packaged, and JavaScript is explicitly unsupported.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where MorphTransition Fits
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/codenameone/CodenameOne/pull/5314" rel="noopener noreferrer"&gt;PR #5314&lt;/a&gt; is listed as a smaller enhancement in the release post, but it connects directly to media generation. &lt;code&gt;MorphTransition&lt;/code&gt; is now scrubbable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;MorphTransition&lt;/span&gt; &lt;span class="n"&gt;transition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MorphTransition&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;snapshotMode&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;opacity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"card"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;scale&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"card"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.8f&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0f&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;rotation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"card"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.2f&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;transition&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setProgress&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scrubbing means a transition can be rendered deterministically at progress &lt;code&gt;0.0&lt;/code&gt;, &lt;code&gt;0.1&lt;/code&gt;, &lt;code&gt;0.2&lt;/code&gt;, and onward. That is exactly what a video export loop needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Platform Backends
&lt;/h2&gt;

&lt;p&gt;The backends use native media stacks rather than one portable lowest-common-denominator codec:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JavaSE and the simulator use bundled ffmpeg/ffprobe.&lt;/li&gt;
&lt;li&gt;iOS and macOS use AVFoundation.&lt;/li&gt;
&lt;li&gt;Android uses MediaCodec, MediaMuxer, MediaExtractor and MediaMetadataRetriever.&lt;/li&gt;
&lt;li&gt;Windows uses Media Foundation.&lt;/li&gt;
&lt;li&gt;Linux uses GStreamer.&lt;/li&gt;
&lt;li&gt;JavaScript uses HTML5 video/canvas decode and WebCodecs encode.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives each platform the codec set and acceleration it already has. App code still starts from the same &lt;code&gt;VideoIO&lt;/code&gt; facade.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Video export used to be something a Codename One app handed off to a backend or a native library. Now the app can render frames, mix PCM, encode a video, decode it back for validation, and generate timed captions. That opens up tutorials, game clips, social exports, support recordings, animated explainers and media QA workflows from the same Java codebase.&lt;/p&gt;

&lt;p&gt;No special revenue model is attached to that. The app generates the media. The app owns the output.&lt;/p&gt;

</description>
      <category>java</category>
      <category>mobile</category>
      <category>android</category>
      <category>ios</category>
    </item>
    <item>
      <title>Versioned Builds Are Back, With Master Builds For Fast Verification</title>
      <dc:creator>Shai Almog</dc:creator>
      <pubDate>Tue, 14 Jul 2026 14:43:46 +0000</pubDate>
      <link>https://dev.to/codenameone/versioned-builds-are-back-with-master-builds-for-fast-verification-2ak7</link>
      <guid>https://dev.to/codenameone/versioned-builds-are-back-with-master-builds-for-fast-verification-2ak7</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F95syzsw5otxflu8woko7.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F95syzsw5otxflu8woko7.jpg" alt="Versioned Builds Are Back, With Master Builds For Fast Verification" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Versioned builds are back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is Codename One?&lt;/strong&gt; Codename One is an open-source framework for building native iOS, Android, desktop, and web apps from a single Java or Kotlin codebase. Learn more at &lt;a href="https://www.codenameone.com/" rel="noopener noreferrer"&gt;codenameone.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The old versioned-build story came from the Ant era. Codename One had point releases every few months, and versioning meant "build against that point release." It made sense at the time, but the platform and the build infrastructure moved on. Maven Central, faster releases, more build targets, and a much larger code surface made the old model hard to maintain.&lt;/p&gt;

&lt;p&gt;The new model keeps the part developers actually need: a way to stabilize and diagnose builds without being forced onto the current server-side framework every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pin A Released Version
&lt;/h2&gt;

&lt;p&gt;By default, a cloud build uses the current Codename One release. With a versioned build, you can pin to a specific published version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;build.cn1Version&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;7.0.182&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The build server fetches that version's framework artifacts and builds against them. If you also want the simulator and local compile classpath to match, set the matching &lt;code&gt;cn1.version&lt;/code&gt; in your Maven project.&lt;/p&gt;

&lt;p&gt;This is useful in three situations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You are stabilizing a production release and want fewer moving parts.&lt;/li&gt;
&lt;li&gt;A build started failing and you need to know whether the regression is in your app or in the framework/build server.&lt;/li&gt;
&lt;li&gt;You are supporting an older customer app and need to reproduce the environment it last built with.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Build Against Master
&lt;/h2&gt;

&lt;p&gt;The more interesting part is &lt;code&gt;master&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;build.cn1Version&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;master&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This builds against the current development head of Codename One. Community developers recently asked for nightly or daily builds, but pushing nightly artifacts through Maven Central and support channels is messier than it sounds. A fixed nightly is also stale the moment the next fix lands.&lt;/p&gt;

&lt;p&gt;Building against &lt;code&gt;master&lt;/code&gt; is a faster developer loop for most verification cases. If a fix landed this morning and you need to verify it on a device, you do not wait for tomorrow's nightly. You ask the cloud build to use the current development head.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRECiAgICBBWyJBIGZpeCBsYW5kcyBvbiBtYXN0ZXIiXSAtLT4gQlsiRnJhbWV3b3JrIGFydGlmYWN0cyBwdWJsaXNoZWQiXQogICAgQiAtLT4gQ1siWW91ciBidWlsZCBzZXRzIGJ1aWxkLmNuMVZlcnNpb249bWFzdGVyIl0KICAgIEMgLS0-IERbIkNsb3VkIGJ1aWxkIHVzZXMgY3VycmVudCBkZXZlbG9wbWVudCBoZWFkIl0KICAgIEQgLS0-IEVbIlZlcmlmeSBvbiByZWFsIHRhcmdldCJdCiAgICBFIC0tPiBGWyJTd2l0Y2ggYmFjayB0byBhIHJlbGVhc2UgZm9yIHByb2R1Y3Rpb24iXQ%3D%3D%3Ftype%3Dpng%26bgColor%3Dffffff" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRECiAgICBBWyJBIGZpeCBsYW5kcyBvbiBtYXN0ZXIiXSAtLT4gQlsiRnJhbWV3b3JrIGFydGlmYWN0cyBwdWJsaXNoZWQiXQogICAgQiAtLT4gQ1siWW91ciBidWlsZCBzZXRzIGJ1aWxkLmNuMVZlcnNpb249bWFzdGVyIl0KICAgIEMgLS0-IERbIkNsb3VkIGJ1aWxkIHVzZXMgY3VycmVudCBkZXZlbG9wbWVudCBoZWFkIl0KICAgIEQgLS0-IEVbIlZlcmlmeSBvbiByZWFsIHRhcmdldCJdCiAgICBFIC0tPiBGWyJTd2l0Y2ggYmFjayB0byBhIHJlbGVhc2UgZm9yIHByb2R1Y3Rpb24iXQ%3D%3D%3Ftype%3Dpng%26bgColor%3Dffffff" alt="Diagram" width="276" height="686"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Use this for verification, not for shipping production builds. &lt;code&gt;master&lt;/code&gt; is active development. It gives you speed, not a stability promise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why It Is Tiered
&lt;/h2&gt;

&lt;p&gt;The small overhead of fetching versioned artifacts is noticeable, but that is not the real reason this is limited by account level.&lt;/p&gt;

&lt;p&gt;The hard cost is support.&lt;/p&gt;

&lt;p&gt;If a free user reports a build issue from six months ago, and another user reports one from yesterday, and an enterprise customer needs a diagnosis against a two-month-old version, support becomes a bisection problem across a moving build system. The further back the community goes, the harder every issue is to understand, reproduce, and fix.&lt;/p&gt;

&lt;p&gt;That does not mean versioned builds should only exist at the top. The new approach opens them much further down than before, including limited access for basic/free usage. The longer windows still belong to higher tiers because those teams are the ones that need older reproducible builds and the support time that comes with them.&lt;/p&gt;

&lt;p&gt;The principle is simple: the tier buys a support window and capacity, not a right to keep your app revenue.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Practical Workflow
&lt;/h2&gt;

&lt;p&gt;When a build starts failing after an update:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# First, verify the last known good release.
&lt;/span&gt;&lt;span class="py"&gt;build.cn1Version&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;7.0.182&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that works, the regression is likely in the framework or build server. If it still fails, the issue is probably in your app, dependencies, native configuration, or a platform toolchain change that affects the old version too.&lt;/p&gt;

&lt;p&gt;When a fix lands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# Then verify the unreleased fix.
&lt;/span&gt;&lt;span class="py"&gt;build.cn1Version&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;master&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the fix is released, pin to the release you plan to ship with or remove the hint to follow the current default again.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Does Not Solve
&lt;/h2&gt;

&lt;p&gt;Versioned builds are not a time machine for every external dependency. Apple, Google, OS SDKs, certificates, signing rules, app store requirements, Maven repositories and native tools all move. A six-month-old framework can still be affected by a current Xcode or Android requirement.&lt;/p&gt;

&lt;p&gt;It also does not make &lt;code&gt;master&lt;/code&gt; a nightly release channel. It is a verification channel.&lt;/p&gt;

&lt;p&gt;That distinction keeps the feature useful without turning support into archaeology for every build ever made.&lt;/p&gt;

</description>
      <category>java</category>
      <category>mobile</category>
      <category>android</category>
      <category>ios</category>
    </item>
    <item>
      <title>Commerce And Secrets Without An IAP Tax</title>
      <dc:creator>Shai Almog</dc:creator>
      <pubDate>Mon, 13 Jul 2026 15:39:12 +0000</pubDate>
      <link>https://dev.to/codenameone/commerce-and-secrets-without-an-iap-tax-1n9b</link>
      <guid>https://dev.to/codenameone/commerce-and-secrets-without-an-iap-tax-1n9b</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyknc8pr9ig13patg4wkc.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyknc8pr9ig13patg4wkc.jpg" alt="Commerce And Secrets Without An IAP Tax" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Commerce is the easiest feature in this release to misunderstand, so the first sentence has to be blunt:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is Codename One?&lt;/strong&gt; Codename One is an open-source framework for building native iOS, Android, desktop, and web apps from a single Java or Kotlin codebase. Learn more at &lt;a href="https://www.codenameone.com/" rel="noopener noreferrer"&gt;codenameone.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Commerce does not replace IAP and never will.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Purchases still go through Apple, Google, or the payment processor you chose. Codename One does not process the payment, does not touch the money, and does not take a percentage. &lt;a href="https://github.com/codenameone/CodenameOne/pull/5300" rel="noopener noreferrer"&gt;PR #5300&lt;/a&gt; adds infrastructure around the annoying backend work that comes after a purchase: validation, entitlement checks, subscription lifecycle, webhooks, and reporting.&lt;/p&gt;

&lt;p&gt;That backend work is real. Anyone who has shipped subscriptions knows the trap. Buying a SKU is not the same as knowing whether the user has the right to a feature right now. Renewals, grace periods, refunds, billing retry, product changes, trials, family sharing and store server notifications all show up later. The device has one view. The store has another. Your backend usually needs a third.&lt;/p&gt;

&lt;p&gt;Commerce is the optional service that turns that mess into an entitlement.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fog539w882pvnk6iphow8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fog539w882pvnk6iphow8.png" alt="Commerce dashboard for receipt validation, entitlements and revenue metrics" width="800" height="591"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRECiAgICBBWyJBcHAgY2FsbHMgQ29tbWVyY2VNYW5hZ2VyLnN1YnNjcmliZSgpIl0gLS0-IEJbIlB1cmNoYXNlIEFQSSJdCiAgICBCIC0tPiBDWyJBcHBsZSAvIEdvb2dsZSBzdG9yZSBmbG93Il0KICAgIEMgLS0-IERbIlN0b3JlIHJlY2VpcHQiXQogICAgRCAtLT4gRVsiQ29tbWVyY2UgcmVmcmVzaCgpIl0KICAgIEUgLS0-IEZbIkNsb3VkIHJlY2VpcHQgdmFsaWRhdGlvbiJdCiAgICBGIC0tPiBHWyJFbnRpdGxlbWVudCBjYWNoZSJdCiAgICBHIC0tPiBIWyJpc0VudGl0bGVkKHBybykiXQogICAgQyAtLT4gU3RvcmVPS1siUHVyY2hhc2Ugc3RpbGwgY29tcGxldGVzIGV2ZW4gaWYgY2xvdWQgdmFsaWRhdGlvbiBpcyB1bmF2YWlsYWJsZSJd%3Ftype%3Dpng%26bgColor%3Dffffff" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRECiAgICBBWyJBcHAgY2FsbHMgQ29tbWVyY2VNYW5hZ2VyLnN1YnNjcmliZSgpIl0gLS0-IEJbIlB1cmNoYXNlIEFQSSJdCiAgICBCIC0tPiBDWyJBcHBsZSAvIEdvb2dsZSBzdG9yZSBmbG93Il0KICAgIEMgLS0-IERbIlN0b3JlIHJlY2VpcHQiXQogICAgRCAtLT4gRVsiQ29tbWVyY2UgcmVmcmVzaCgpIl0KICAgIEUgLS0-IEZbIkNsb3VkIHJlY2VpcHQgdmFsaWRhdGlvbiJdCiAgICBGIC0tPiBHWyJFbnRpdGxlbWVudCBjYWNoZSJdCiAgICBHIC0tPiBIWyJpc0VudGl0bGVkKHBybykiXQogICAgQyAtLT4gU3RvcmVPS1siUHVyY2hhc2Ugc3RpbGwgY29tcGxldGVzIGV2ZW4gaWYgY2xvdWQgdmFsaWRhdGlvbiBpcyB1bmF2YWlsYWJsZSJd%3Ftype%3Dpng%26bgColor%3Dffffff" alt="Diagram" width="515" height="870"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Entitlements Instead Of SKU Branches
&lt;/h2&gt;

&lt;p&gt;Your app should not need to know every SKU that grants &lt;code&gt;pro&lt;/code&gt;. It should ask for &lt;code&gt;pro&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;CommerceManager&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CommerceManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setAppUserId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accountId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEntitled&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"pro"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;unlockProFeatures&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Purchases are still delegated to the existing &lt;code&gt;Purchase&lt;/code&gt; API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;subscribe&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"pro_monthly"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// or&lt;/span&gt;
&lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;purchase&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"remove_ads"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After a purchase, or when the app starts, refresh off the EDT:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="o"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;CommerceManager&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CommerceManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;refresh&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="no"&gt;CN&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;callSerially&lt;/span&gt;&lt;span class="o"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEntitled&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"pro"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;unlockProFeatures&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;});&lt;/span&gt;
&lt;span class="o"&gt;}).&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;refresh()&lt;/code&gt; validates the current receipts with the cloud when the build has a &lt;code&gt;build_key&lt;/code&gt; and commerce is enabled. In a local build or simulator, it safely falls back to the normal &lt;code&gt;Purchase&lt;/code&gt; path.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happens When Quota Runs Out
&lt;/h2&gt;

&lt;p&gt;This is the real question that matters most. If Commerce is tiered, what happens when a developer exceeds quota?&lt;/p&gt;

&lt;p&gt;Validation degrades. Purchases do not stop.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CommerceManager.isDegraded()&lt;/code&gt; tells you the cloud did not return a server-validated answer. In that state, entitlement checks fall back to the platform's own receipt signal, treating the entitlement id as a subscription SKU when no cached cloud answer exists. That is less rich than server-side validation, but it is the right failure mode. A paying user should not be locked out because your account hit a validated-volume cap.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;refresh&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isDegraded&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;p&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Commerce validation degraded; using store-direct fallback"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commerce is tiered because it is a backend service that can be abused: receipt validation, store API calls, lifecycle processing, webhook delivery and revenue analytics cost real infrastructure. The degradation rule is what keeps that business reality from becoming user pain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Commerce Adds
&lt;/h2&gt;

&lt;p&gt;The service can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Validate receipts against Apple and Google.&lt;/li&gt;
&lt;li&gt;Normalize subscription state across stores.&lt;/li&gt;
&lt;li&gt;Track entitlements by your app user id.&lt;/li&gt;
&lt;li&gt;Forward lifecycle webhooks to your backend with HMAC signatures.&lt;/li&gt;
&lt;li&gt;Present revenue metrics such as MRR, ARR, ARPU, churn, trial conversion, cohort retention and realized LTV.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The app-facing API remains small because the complicated part lives server-side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;CommerceManager&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CommerceManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setAppUserId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;myAccountId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;refresh&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEntitled&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"remove_ads"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That split explains how Commerce complements IAP instead of replacing it. &lt;code&gt;Purchase&lt;/code&gt; starts the transaction. Commerce answers the longer-term entitlement question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Secrets
&lt;/h2&gt;

&lt;p&gt;The same PR adds &lt;code&gt;com.codename1.security.Secrets&lt;/code&gt;, which solves a different but related problem: API keys do not belong in source code or in the app binary.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw8rt1119afvi8n97844n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw8rt1119afvi8n97844n.png" alt="Secrets dashboard for managing app-readable secrets and server-side credentials" width="800" height="534"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Run off the EDT; the first call may hit the network.&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;mapsKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Secrets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"googlemaps.key"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The value is fetched from the Codename One Cloud vault over TLS and cached in &lt;code&gt;SecureStorage&lt;/code&gt;. &lt;code&gt;refresh(name)&lt;/code&gt; forces a fresh fetch after you rotate the value server-side, and &lt;code&gt;clear(name)&lt;/code&gt; drops the cached copy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Secrets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;refresh&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"googlemaps.key"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;Secrets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;clear&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"googlemaps.key"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only app-readable secrets are served to the device. Server-only credentials, such as App Store Connect keys or Google Play service account JSON used for commerce validation, stay in the vault and are never reachable from client code.&lt;/p&gt;

&lt;p&gt;That rule is non-negotiable: do not check API keys into source, do not paste them into snippets, and do not embed server credentials in the binary. If the app can read a secret, a determined attacker can eventually extract it. Secrets reduces exposure and makes rotation easier; it does not turn a client app into a secure server.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Boundary
&lt;/h2&gt;

&lt;p&gt;Commerce and Secrets are both cloud features, but they sit on different sides of the volume line. Secrets usage stays low enough to enable it for everyone. Commerce has tiers because validation and analytics can create real backend load.&lt;/p&gt;

&lt;p&gt;The important boundary is not tiering. The boundary is lock-in. You can still use the raw &lt;code&gt;Purchase&lt;/code&gt; API. You can still build your own receipt backend. You can still ship an app that sells subscriptions without giving Codename One a revenue share.&lt;/p&gt;

&lt;p&gt;Commerce is there to remove backend pain, not to insert a toll booth.&lt;/p&gt;

</description>
      <category>java</category>
      <category>mobile</category>
      <category>android</category>
      <category>ios</category>
    </item>
    <item>
      <title>Motion, Stylus, Trackpads And Foldables In The Core</title>
      <dc:creator>Shai Almog</dc:creator>
      <pubDate>Sun, 12 Jul 2026 14:23:42 +0000</pubDate>
      <link>https://dev.to/codenameone/motion-stylus-trackpads-and-foldables-in-the-core-4m87</link>
      <guid>https://dev.to/codenameone/motion-stylus-trackpads-and-foldables-in-the-core-4m87</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8tjad9hxrpn01ja0dtz3.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8tjad9hxrpn01ja0dtz3.jpg" alt="Motion, Stylus, Trackpads And Foldables In The Core" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For a long time, mobile meant a touch screen and a few platform-specific escape hatches. That is no longer enough. A Codename One app can run on phones, tablets, desktops, watches, TVs, browsers, foldables, touch laptops, external displays, cars, and devices with pens, mice, and trackpads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is Codename One?&lt;/strong&gt; Codename One is an open-source framework for building native iOS, Android, desktop, and web apps from a single Java or Kotlin codebase. Learn more at &lt;a href="https://www.codenameone.com/" rel="noopener noreferrer"&gt;codenameone.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/codenameone/CodenameOne/pull/5310" rel="noopener noreferrer"&gt;PR #5310&lt;/a&gt; and &lt;a href="https://github.com/codenameone/CodenameOne/pull/5309" rel="noopener noreferrer"&gt;PR #5309&lt;/a&gt; make that hardware visible through core APIs instead of cn1libs and platform branches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Motion Sensors And Gestures
&lt;/h2&gt;

&lt;p&gt;The new &lt;code&gt;com.codename1.sensors&lt;/code&gt; package exposes accelerometer, gyroscope and magnetometer readings. It also derives gravity, linear acceleration and orientation in the core when the platform does not provide them directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;MotionSensorManager&lt;/span&gt; &lt;span class="n"&gt;sensors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MotionSensorManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="nc"&gt;MotionSensor&lt;/span&gt; &lt;span class="n"&gt;accel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sensors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSensor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MotionSensorManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TYPE_ACCELEROMETER&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accel&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;MotionSensorListener&lt;/span&gt; &lt;span class="n"&gt;listener&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MotionSensorListener&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;motionReceived&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MotionEvent&lt;/span&gt; &lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setText&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getX&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;", "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getY&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;", "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getZ&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
            &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getParent&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;revalidate&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;};&lt;/span&gt;
    &lt;span class="n"&gt;accel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addListener&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;listener&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The manager itself is always available. Individual sensors return &lt;code&gt;null&lt;/code&gt; when the hardware is unsupported, so the app can keep one code path with a small capability check.&lt;/p&gt;

&lt;p&gt;Gestures are built on top of the sensor stream in the core:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;MotionSensorManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;addGestureListener&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;GestureEvent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TYPE_SHAKE&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;GestureListener&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;gestureDetected&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;GestureEvent&lt;/span&gt; &lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;Dialog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;show&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Shaken"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"You shook the device"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"OK"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The recognized set is practical rather than exotic: shake, flip face down, flip face up, tilt left, tilt right, tilt forward, tilt backward, pick up and free fall. Because recognition lives in &lt;code&gt;GestureEngine&lt;/code&gt;, every port that exposes an accelerometer gets the same behavior.&lt;/p&gt;

&lt;p&gt;Sampling is reference counted. The hardware powers on while listeners exist and powers down when they are removed. On iOS you still need &lt;code&gt;ios.NSMotionUsageDescription&lt;/code&gt; with a user-facing reason for motion access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rich Pointer Detail
&lt;/h2&gt;

&lt;p&gt;Classic pointer callbacks tell you where a press happened. Modern input asks more questions: was it a mouse, a finger, a stylus, or an eraser? Which button? What pressure? Was the pen tilted? Was the pointer hovering?&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PointerEvent&lt;/code&gt; is the snapshot attached to pointer action events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;myComponent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addPointerPressedListener&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;PointerEvent&lt;/span&gt; &lt;span class="n"&gt;pe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPointerEvent&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isSecondaryButton&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;showContextMenu&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getX&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;pe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getY&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;drawPreview&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getX&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;pe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getY&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;pe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPressure&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also poll the current dispatch state through &lt;code&gt;CN&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;CN&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPointerButton&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;pressure&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;CN&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPointerPressure&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="n"&gt;pen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;CN&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isStylusPointer&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="nc"&gt;PointerEvent&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;CN&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getCurrentPointerEvent&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On plain touch screens these values have safe defaults: primary button, pressure &lt;code&gt;1.0&lt;/code&gt;, touch or unknown pointer type. Existing apps keep working.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wheel, Trackpad And Context Menus
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;WheelEvent&lt;/code&gt; is now the cross-platform scroll-gesture event. It handles mouse wheels, precision trackpads, horizontal scrolling, iOS/iPadOS pointer devices and the Apple Watch Digital Crown through the same path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;canvas&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addMouseWheelListener&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;WheelEvent&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;WheelEvent&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isControlDown&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;zoom&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDeltaY&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
        &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;consume&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Consuming the event suppresses the default scroll. That is the difference between "wheel scrolls the container" and "control plus wheel zooms a design surface."&lt;/p&gt;

&lt;p&gt;For context menus, use the higher-level listener:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addContextMenuListener&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;consume&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;showMyContextMenu&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getX&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getY&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It fires for right click, stylus barrel button and long press, which is exactly the kind of cross-platform behavior app code should not have to rediscover.&lt;/p&gt;

&lt;p&gt;Trackpad magnify and rotate gestures route to component methods:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Container&lt;/span&gt; &lt;span class="n"&gt;photo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Container&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;pinch&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;zoom&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;rotation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;radians&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;rotate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;radians&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Stylus And Foldables
&lt;/h2&gt;

&lt;p&gt;Stylus support exposes pressure, tilt, contact size and eraser type. A drawing surface can listen only to stylus events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;drawingArea&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addStylusListener&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;PointerEvent&lt;/span&gt; &lt;span class="n"&gt;pe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPointerEvent&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;pe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPressure&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEraser&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;erase&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getX&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;pe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getY&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;drawPoint&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getX&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;pe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getY&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Foldables use &lt;code&gt;DevicePosture&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;DevicePosture&lt;/span&gt; &lt;span class="n"&gt;posture&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;CN&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDevicePosture&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;posture&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isFoldable&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;posture&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isTableTop&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;layoutForTableTop&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;posture&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getFoldBounds&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="no"&gt;CN&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addPostureListener&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;relayoutForPosture&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;CN&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDevicePosture&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Android, foldable support is opt-in with &lt;code&gt;android.foldableSupport=true&lt;/code&gt;, so apps that do not need the AndroidX window library do not carry it. The simulator can generate posture changes for testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Input used to mean key codes, pointer coordinates, and maybe a scroll wheel. That model is too small now. A modern app has to reason about pointer type, pressure, hover, buttons, tilt, posture, trackpads, sensor sampling, and gestures that may or may not exist on the current device.&lt;/p&gt;

&lt;p&gt;The new APIs do not make that complexity disappear. They put it behind capability checks and default-safe events, so the same component can handle a finger on a phone, a stylus on a tablet, a mouse on desktop, and a foldable hinge without turning every screen into platform-specific code.&lt;/p&gt;

</description>
      <category>java</category>
      <category>mobile</category>
      <category>android</category>
      <category>ios</category>
    </item>
    <item>
      <title>CarPlay And Android Auto From One Codename One API</title>
      <dc:creator>Shai Almog</dc:creator>
      <pubDate>Sat, 11 Jul 2026 14:27:46 +0000</pubDate>
      <link>https://dev.to/codenameone/carplay-and-android-auto-from-one-codename-one-api-32b6</link>
      <guid>https://dev.to/codenameone/carplay-and-android-auto-from-one-codename-one-api-32b6</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8iksl7bpgx2dodf4ngsj.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8iksl7bpgx2dodf4ngsj.jpg" alt="CarPlay And Android Auto From One Codename One API" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Yesterday's release post was about the bigger business line we will not cross: no royalties on IAP, ads, commerce or app revenue. This post is about one of the most concrete platform additions in that release. &lt;a href="https://github.com/codenameone/CodenameOne/pull/5281" rel="noopener noreferrer"&gt;PR #5281&lt;/a&gt; adds Apple CarPlay and Google Android Auto support under &lt;code&gt;com.codename1.car&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is Codename One?&lt;/strong&gt; Codename One is an open-source framework for building native iOS, Android, desktop, and web apps from a single Java or Kotlin codebase. Learn more at &lt;a href="https://www.codenameone.com/" rel="noopener noreferrer"&gt;codenameone.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The first thing to know is what this is not. CarPlay and Android Auto are not second screens where your normal Codename One &lt;code&gt;Form&lt;/code&gt; is drawn. They are driver-safe, template-based systems. Apple and Google decide which templates are legal in a car, how many rows can appear, which app categories are allowed, and which interactions are safe while driving.&lt;/p&gt;

&lt;p&gt;Codename One now gives you one portable way to describe those templates.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy6n2bvgh30unf9hq8emj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy6n2bvgh30unf9hq8emj.png" alt="A CarPlay list template generated from the portable car model" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg4etnehk7wm3t0iexzcc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg4etnehk7wm3t0iexzcc.png" alt="An Android Auto grid template generated from the same portable car model" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Template Model
&lt;/h2&gt;

&lt;p&gt;A car app registers a &lt;code&gt;CarApplication&lt;/code&gt;. When a head unit connects, the application returns a root &lt;code&gt;CarScreen&lt;/code&gt;. Each screen returns one &lt;code&gt;CarTemplate&lt;/code&gt;: list, grid, pane, message, navigation, or now-playing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.codename1.car.*&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyApp&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Object&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Car&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setApplication&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MyCarApplication&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyCarApplication&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;CarApplication&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;CarScreen&lt;/span&gt; &lt;span class="nf"&gt;onCreateRootScreen&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CarContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;LibraryScreen&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LibraryScreen&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;CarScreen&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="nc"&gt;CarTemplate&lt;/span&gt; &lt;span class="nf"&gt;onCreateTemplate&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CarListTemplate&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;setTitle&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Library"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addRow&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CarRow&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Now Playing"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setText&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Daft Punk - Discovery"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setOnAction&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;play&lt;/span&gt;&lt;span class="o"&gt;()))&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addRow&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CarRow&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Albums"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setBrowsable&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setOnAction&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pushScreen&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AlbumsScreen&lt;/span&gt;&lt;span class="o"&gt;())))&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addRow&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CarRow&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Playlists"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setBrowsable&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setOnAction&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pushScreen&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PlaylistsScreen&lt;/span&gt;&lt;span class="o"&gt;())));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That code maps to &lt;code&gt;CPListTemplate&lt;/code&gt; on CarPlay and &lt;code&gt;ListTemplate&lt;/code&gt; on Android Auto. Grid items map to &lt;code&gt;CPGridButton&lt;/code&gt; and &lt;code&gt;GridItem&lt;/code&gt;. The now-playing template routes to the system media surface. Navigation gets a template shell with controls and ETA strips; the moving map surface is scaffolded but still being completed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Navigation And Lifecycle
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;CarContext&lt;/code&gt; gives you a head-unit stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pushScreen&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DetailScreen&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;popScreen&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;screen&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invalidate&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;showToast&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Added to queue"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;CarScreen&lt;/code&gt; has lifecycle hooks for create, resume, pause, and destroy. &lt;code&gt;CarApplication&lt;/code&gt; receives connect and disconnect callbacks, and you can listen globally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Car&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addConnectionListener&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CarConnectionListener&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;carConnected&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CarContext&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;startLocationStream&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;carDisconnected&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;stopLocationStream&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a good place to start and stop work that only exists while the dashboard is connected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zero Cost When Unused
&lt;/h2&gt;

&lt;p&gt;The build system scans your bytecode. If it sees &lt;code&gt;com.codename1.car&lt;/code&gt;, it injects the native wiring. If it does not, the app carries none of this.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRECiAgQVsiWW91ciBhcHAgYnl0ZWNvZGUiXSAtLT4gQnsiUmVmZXJlbmNlcyBjb20uY29kZW5hbWUxLmNhcj8ifQogIEIgLS0-fG5vfCBDWyJOb3JtYWwgaU9TIC8gQW5kcm9pZCBidWlsZDxici8-bm8gY2FyIGRlcGVuZGVuY2llcyJdCiAgQiAtLT58eWVzfCBEWyJJbmplY3QgY2FyIHN1cHBvcnQiXQogIEQgLS0-IEVbImlPUzogQ2FyUGxheSBzY2VuZTxici8-ZnJhbWV3b3JrICsgZW50aXRsZW1lbnQiXQogIEQgLS0-IEZbIkFuZHJvaWQ6IENhckFwcFNlcnZpY2U8YnIvPmFuZHJvaWR4LmNhci5hcHAgZGVwZW5kZW5jeSJdCiAgRSAtLT4gR1siTmF0aXZlIGhlYWQgdW5pdCB0ZW1wbGF0ZXMiXQogIEYgLS0-IEc%3D%3Ftype%3Dpng%26bgColor%3Dffffff" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRECiAgQVsiWW91ciBhcHAgYnl0ZWNvZGUiXSAtLT4gQnsiUmVmZXJlbmNlcyBjb20uY29kZW5hbWUxLmNhcj8ifQogIEIgLS0-fG5vfCBDWyJOb3JtYWwgaU9TIC8gQW5kcm9pZCBidWlsZDxici8-bm8gY2FyIGRlcGVuZGVuY2llcyJdCiAgQiAtLT58eWVzfCBEWyJJbmplY3QgY2FyIHN1cHBvcnQiXQogIEQgLS0-IEVbImlPUzogQ2FyUGxheSBzY2VuZTxici8-ZnJhbWV3b3JrICsgZW50aXRsZW1lbnQiXQogIEQgLS0-IEZbIkFuZHJvaWQ6IENhckFwcFNlcnZpY2U8YnIvPmFuZHJvaWR4LmNhci5hcHAgZGVwZW5kZW5jeSJdCiAgRSAtLT4gR1siTmF0aXZlIGhlYWQgdW5pdCB0ZW1wbGF0ZXMiXQogIEYgLS0-IEc%3D%3Ftype%3Dpng%26bgColor%3Dffffff" alt="Diagram" width="684" height="806"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is especially important for a framework with one codebase. A normal shopping app, game, or business tool should not gain a CarPlay dependency just because the framework knows how to build one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build Hints And Approval
&lt;/h2&gt;

&lt;p&gt;Car app categories matter. The build can inject wiring, but Apple and Google still decide whether an app is allowed into the car.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;ios.carplay.audio&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;ios.carplay.navigation&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;android.androidAuto.navigation&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;android.androidAuto.poi&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Audio is the default CarPlay category when no iOS car category is specified. Messaging, navigation and point-of-interest categories need the matching hints. Restricted Android Auto categories and real CarPlay deployment still require platform approval and, for iOS, the relevant CarPlay entitlement on your Apple App ID.&lt;/p&gt;

&lt;p&gt;That is not a Codename One limitation. It is the safety model of the car platforms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Simulator Head Units
&lt;/h2&gt;

&lt;p&gt;The simulator has a &lt;code&gt;Car&lt;/code&gt; menu now. &lt;code&gt;Car &amp;gt; Connect CarPlay&lt;/code&gt; and &lt;code&gt;Car &amp;gt; Connect Android Auto&lt;/code&gt; open a simulated head-unit window that renders the same portable template tree. It is not a pixel-perfect emulator, but it gives you a fast loop for stack navigation, row caps and action callbacks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpbo3oyku2mp19wmvjnc2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpbo3oyku2mp19wmvjnc2.png" alt="The Codename One simulator rendering a CarPlay-style list head unit" width="720" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Selecting a browsable row pushes the next screen:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fob2b5xst6ve4a9n0x5fx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fob2b5xst6ve4a9n0x5fx.png" alt="The Codename One simulator rendering a CarPlay-style grid head unit" width="720" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is the right level of simulation. You can build the car experience while still working in the normal desktop loop, then use Apple's CarPlay simulator or Android's Desktop Head Unit when you need to test the native projection layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;CarPlay and Android Auto support is a platform capability, not a revenue feature. There is no extra royalty because your podcast app, navigation app, or point-of-interest app reaches a dashboard. You still need Apple and Google approval for the categories they restrict, and you still need to design within the car template catalogue. Codename One now gives you the portable model and build wiring so that work lives in one codebase.&lt;/p&gt;

</description>
      <category>java</category>
      <category>mobile</category>
      <category>android</category>
      <category>ios</category>
    </item>
    <item>
      <title>More Surfaces, Same Deal: Cars, Sensors, Commerce, Video And Builds</title>
      <dc:creator>Shai Almog</dc:creator>
      <pubDate>Fri, 10 Jul 2026 15:27:50 +0000</pubDate>
      <link>https://dev.to/codenameone/more-surfaces-same-deal-cars-sensors-commerce-video-and-builds-29pg</link>
      <guid>https://dev.to/codenameone/more-surfaces-same-deal-cars-sensors-commerce-video-and-builds-29pg</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmyuklmlb2gsffcnlu61z.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmyuklmlb2gsffcnlu61z.jpg" alt="More Surfaces, Same Deal: Cars, Sensors, Commerce, Video And Builds" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Last week's release post was about funding open source without the bait and switch. This week's release tests that idea again, because two of the new features touch paid infrastructure directly: Commerce and versioned builds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is Codename One?&lt;/strong&gt; Codename One is an open-source framework for building native iOS, Android, desktop, and web apps from a single Java or Kotlin codebase. Learn more at &lt;a href="https://www.codenameone.com/" rel="noopener noreferrer"&gt;codenameone.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That kind of expansion often makes the model slippery. A platform adds something developers need, puts it behind an account tier, and over time the open part gets worse or the paid part starts taxing success. We do not want to drift into that, so the useful question for this week is not just what shipped, but how the paid pieces behave.&lt;/p&gt;

&lt;p&gt;Commerce validates purchases and normalizes entitlements, but it does not replace IAP and it does not take a cut. If quota runs out, validation degrades; the Apple or Google purchase still goes through. Everyone gets the Secrets API because volume stays low, and keeping keys out of source code should not be a luxury feature. Versioned builds are back, including limited access lower down the account ladder, and master builds give the community a way to verify fixes without waiting for nightly artifacts.&lt;/p&gt;

&lt;p&gt;That is the continuation of last week's deal: charge for services that cost money to run, but keep them optional, predictable, and as broadly useful as we can make them.&lt;/p&gt;

&lt;p&gt;The rest of the release pushes the open source core outward: car projection, motion sensors, richer pointer input, foldable posture, frame-accurate video, sample-accurate PCM mixing, and timed subtitles. A phone screen is still the center of most apps, but it is no longer the whole app. A serious product may also need a CarPlay list, an Android Auto grid, a stylus drawing surface, trackpad zoom, motion gestures, a pinned production build, and a generated video clip for sharing or support.&lt;/p&gt;

&lt;p&gt;Here is what shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  CarPlay and Android Auto
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/codenameone/CodenameOne/pull/5281" rel="noopener noreferrer"&gt;PR #5281&lt;/a&gt; adds a portable &lt;code&gt;com.codename1.car&lt;/code&gt; API for Apple CarPlay and Google Android Auto. The important caveat is that car platforms are template-based. They do not allow an app to draw an arbitrary Codename One &lt;code&gt;Form&lt;/code&gt; on the dashboard. You describe a driver-safe list, grid, message, pane, navigation, or now-playing template, and Codename One maps that to &lt;code&gt;CPTemplate&lt;/code&gt; on CarPlay and &lt;code&gt;androidx.car.app&lt;/code&gt; templates on Android Auto.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy6n2bvgh30unf9hq8emj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy6n2bvgh30unf9hq8emj.png" alt="CarPlay list template rendered by the Codename One car API" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg4etnehk7wm3t0iexzcc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg4etnehk7wm3t0iexzcc.png" alt="Android Auto grid template rendered by the Codename One car API" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The API is zero cost when unused. Referencing &lt;code&gt;com.codename1.car&lt;/code&gt; is what tells the build to inject CarPlay scenes, entitlements, Android Auto services, and the AndroidX dependency. Apps that never touch the package do not carry that code. Tomorrow's post walks through the template model, the simulator head unit, and the approvals you still need from Apple and Google.&lt;/p&gt;

&lt;h2&gt;
  
  
  Motion, Input, And Real Hardware
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/codenameone/CodenameOne/pull/5310" rel="noopener noreferrer"&gt;PR #5310&lt;/a&gt; adds &lt;code&gt;com.codename1.sensors&lt;/code&gt;, a cross-platform motion API with accelerometer, gyroscope, magnetometer, derived gravity, linear acceleration, orientation, and common gestures such as shake, flip, tilt, pick up, and free fall. This replaces the old external &lt;code&gt;sensors-codenameone&lt;/code&gt; cn1lib with a core API and a core gesture engine.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/codenameone/CodenameOne/pull/5309" rel="noopener noreferrer"&gt;PR #5309&lt;/a&gt; fills in the other side of modern hardware: rich pointer events, mouse buttons, wheel and trackpad scrolling, stylus pressure and tilt, foldable posture, desktop windowing, and external display awareness.&lt;/p&gt;

&lt;p&gt;Together they make Codename One less phone-only. A canvas can tell the difference between a finger, a mouse, and a stylus. A foldable can split layout around a hinge. A trackpad pinch can zoom the same component as a mobile pinch. Sunday's post covers the full hardware story.&lt;/p&gt;

&lt;h2&gt;
  
  
  Commerce And Secrets
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/codenameone/CodenameOne/pull/5300" rel="noopener noreferrer"&gt;PR #5300&lt;/a&gt; adds the Commerce SDK and the Secrets API.&lt;/p&gt;

&lt;p&gt;Commerce is optional infrastructure around IAP. It answers the backend question apps usually end up writing themselves: "does this user have this entitlement right now?" It can validate receipts, normalize subscription state across Apple and Google, send lifecycle webhooks to your backend, and show revenue metrics. It does not replace &lt;code&gt;Purchase&lt;/code&gt;, it delegates to it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;CommerceManager&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CommerceManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setAppUserId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accountId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;subscribe&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"pro_monthly"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Thread&lt;/span&gt;&lt;span class="o"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;refresh&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEntitled&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"pro"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;unlockProFeatures&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}).&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Secrets API has deliberately lower volume and is enabled for everyone. It fetches app-readable secrets from the cloud vault at runtime and caches them in &lt;code&gt;SecureStorage&lt;/code&gt;, so API keys do not need to live in code or in the binary. Server-only credentials, such as App Store or Google Play keys used by commerce validation, stay server-side and are not served to the app.&lt;/p&gt;

&lt;p&gt;Monday's post is mostly a failure-mode pass over Commerce: what it does, what it refuses to do, what happens when quota is exhausted, and why optional validation is not an IAP tax.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fog539w882pvnk6iphow8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fog539w882pvnk6iphow8.png" alt="Commerce dashboard for receipt validation and entitlement tracking" width="800" height="591"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw8rt1119afvi8n97844n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw8rt1119afvi8n97844n.png" alt="Secrets dashboard for cloud-managed app secrets" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Versioned Builds Are Back
&lt;/h2&gt;

&lt;p&gt;Versioned builds are back, and the model is better suited to Maven than the old Ant-era point release scheme. You can pin a cloud build to a released Codename One version, or build against &lt;code&gt;master&lt;/code&gt; when you need to verify an unreleased fix.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;build.cn1Version&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;master&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Community developers recently asked for nightly or daily builds. Building against master solves the same verification problem without waiting for an artificial daily package. You are asking the build server to use the current development head.&lt;/p&gt;

&lt;p&gt;Subscription tiers limit versioned build access because old versions create support churn, not because fetching a few artifacts drives the business model. The further back a build goes, the harder it is to diagnose a regression against the current code. The new model still opens versioned builds much further down the account ladder than before, including limited access for basic/free usage, while giving paying teams the longer support windows they actually need. Tuesday's post covers the tradeoff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Video, Audio, And Subtitles
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/codenameone/CodenameOne/pull/5315" rel="noopener noreferrer"&gt;PR #5315&lt;/a&gt; adds &lt;code&gt;VideoIO&lt;/code&gt;: cross-platform video encode and frame-accurate decode using native codecs. It can encode app-rendered frames plus audio into MP4/WebM/MOV/MKV style containers, enumerate codecs, decode exact frames, resample variable-frame-rate clips, and expose the audio track as PCM.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/codenameone/CodenameOne/pull/5317" rel="noopener noreferrer"&gt;PR #5317&lt;/a&gt; adds a sample-accurate &lt;code&gt;AudioMixer&lt;/code&gt; for combining PCM tracks on one clock. &lt;a href="https://github.com/codenameone/CodenameOne/pull/5319" rel="noopener noreferrer"&gt;PR #5319&lt;/a&gt; adds timed Whisper transcription, so generated videos can get SRT or VTT captions instead of plain text transcripts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F15w3r985kye1kn5y28lv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F15w3r985kye1kn5y28lv.png" alt="Frame-accurate VideoIO decode output from the Codename One test app" width="800" height="1736"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Wednesday's post shows how these pieces fit: render frames, mix audio, encode a video, decode frames back out, and attach timed captions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smaller But Important
&lt;/h2&gt;

&lt;p&gt;There are four smaller changes worth calling out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JavaSE upstream JCEF and bundled ffmpeg media.&lt;/strong&gt; &lt;a href="https://github.com/codenameone/CodenameOne/pull/5294" rel="noopener noreferrer"&gt;PR #5294&lt;/a&gt; moves the simulator away from our home-built Chromium 84 CEF fork and onto upstream prebuilt JCEF. We used to build our own CEF fork mostly to keep H.264 media working. That build was long, fragile and kept us behind. The new approach uses upstream JCEF for the browser and bundled ffmpeg for the Codename One &lt;code&gt;Media&lt;/code&gt; API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skin Designer now builds with our JavaScript port.&lt;/strong&gt; &lt;a href="https://github.com/codenameone/CodenameOne/pull/5303" rel="noopener noreferrer"&gt;PR #5303&lt;/a&gt; moves Skin Designer to the local ParparVM JavaScript target. Initializr and Playground already moved; the console is now the last TeaVM internal tool. This migration did expose a regression, but the JavaScript port is still moving toward production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MorphTransition grew up.&lt;/strong&gt; &lt;a href="https://github.com/codenameone/CodenameOne/pull/5314" rel="noopener noreferrer"&gt;PR #5314&lt;/a&gt; adds opacity, rotation, scale, deterministic scrubbing, and arbitrary rendered elements. This matters for UI polish and also for frame export, because scrubbing lets the same transition be rendered into generated video.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timed Whisper transcription.&lt;/strong&gt; &lt;a href="https://github.com/codenameone/CodenameOne/pull/5319" rel="noopener noreferrer"&gt;PR #5319&lt;/a&gt; also deserves a second mention because timed segments turn transcription into subtitles. The cn1lib can render SRT and VTT payloads from segment timestamps.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Upcoming Posts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Saturday.&lt;/strong&gt; . PR &lt;a href="https://github.com/codenameone/CodenameOne/pull/5281" rel="noopener noreferrer"&gt;#5281&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sunday.&lt;/strong&gt; . PRs &lt;a href="https://github.com/codenameone/CodenameOne/pull/5310" rel="noopener noreferrer"&gt;#5310&lt;/a&gt; and &lt;a href="https://github.com/codenameone/CodenameOne/pull/5309" rel="noopener noreferrer"&gt;#5309&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monday.&lt;/strong&gt; . PR &lt;a href="https://github.com/codenameone/CodenameOne/pull/5300" rel="noopener noreferrer"&gt;#5300&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tuesday.&lt;/strong&gt; .&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wednesday.&lt;/strong&gt; . PRs &lt;a href="https://github.com/codenameone/CodenameOne/pull/5315" rel="noopener noreferrer"&gt;#5315&lt;/a&gt;, &lt;a href="https://github.com/codenameone/CodenameOne/pull/5317" rel="noopener noreferrer"&gt;#5317&lt;/a&gt;, and &lt;a href="https://github.com/codenameone/CodenameOne/pull/5319" rel="noopener noreferrer"&gt;#5319&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thursday.&lt;/strong&gt; . The final part of the Game Builder tutorial series.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;The release is large because the app surface is larger now. It reaches cars, foldables, desktops, sensors, stylus devices, media pipelines, cloud validation, and older build timelines.&lt;/p&gt;

&lt;p&gt;The business side needs the same discipline as the API side. Cloud validation should help, not hold purchases hostage. Build tiers should buy capacity and support windows, not a license to keep your own revenue. Optional should mean optional, especially when the project stays open source.&lt;/p&gt;

</description>
      <category>java</category>
      <category>mobile</category>
      <category>android</category>
      <category>ios</category>
    </item>
    <item>
      <title>Game Builder Tutorial 2: Build a Blackjack Card Game (Duke Jack)</title>
      <dc:creator>Shai Almog</dc:creator>
      <pubDate>Thu, 09 Jul 2026 15:53:12 +0000</pubDate>
      <link>https://dev.to/codenameone/game-builder-tutorial-2-build-a-blackjack-card-game-duke-jack-gha</link>
      <guid>https://dev.to/codenameone/game-builder-tutorial-2-build-a-blackjack-card-game-duke-jack-gha</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7pm4vfsbh918u9ryz3l3.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7pm4vfsbh918u9ryz3l3.jpg" alt="Duke Jack, a blackjack game built with the Game Builder" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://www.codenameone.com/blog/game-builder-2d-platformer/" rel="noopener noreferrer"&gt;Tutorial 1&lt;/a&gt; Duke dashed for coffee with arcade physics. Now he sets the cup down for a calmer contest: &lt;strong&gt;Duke Jack&lt;/strong&gt;, a game of blackjack. A card game has none of that arcade motion — cards sit on the felt and the &lt;em&gt;rules&lt;/em&gt; decide who wins. This tutorial shows how the same Game Builder pattern (visual data + an &lt;code&gt;onUpdate&lt;/code&gt; companion) handles a card game, where your code reads the cards and runs the table instead of simulating movement. We'll build a felt table, deal a real hand, and wire up the complete blackjack rules: hit, stand, the dealer's draw, and the win/lose decision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is Codename One?&lt;/strong&gt; Codename One is an open-source framework for building native iOS, Android, desktop, and web apps from a single Java or Kotlin codebase. Learn more at &lt;a href="https://www.codenameone.com/" rel="noopener noreferrer"&gt;codenameone.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you haven't set up a project yet, the &lt;a href="https://www.codenameone.com/blog/game-builder-2d-platformer/#step-0-create-the-project-and-scaffold-a-scene" rel="noopener noreferrer"&gt;project setup in Tutorial 1&lt;/a&gt; applies verbatim — only the mode changes (board mode isn't the default, so the &lt;code&gt;-Dmode=board&lt;/code&gt; flag is required here):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mvn cn1:create-game-scene &lt;span class="nt"&gt;-DclassName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;com.example.dukejack.DukeJack &lt;span class="nt"&gt;-Dmode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;board
mvn cn1:gamebuilder
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why board mode for cards?
&lt;/h2&gt;

&lt;p&gt;Board mode is the Game Builder's grid mode: you place elements on a flat board of cells instead of a free-scrolling world. That's a natural fit for a card table — the felt is a tile layer, and each card is an element you position by hand, carrying its own &lt;code&gt;rank&lt;/code&gt;, &lt;code&gt;suit&lt;/code&gt; and &lt;code&gt;faceUp&lt;/code&gt; data. There's no physics and no camera to chase; the layout &lt;em&gt;is&lt;/em&gt; the game state, and your rules read it. (Board mode can also tilt the grid into an isometric view through &lt;code&gt;IsoProjection&lt;/code&gt; for tabletop games — for cards we keep it flat and top-down.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 — A card-table scene
&lt;/h2&gt;

&lt;p&gt;Pick &lt;strong&gt;New scene → Board&lt;/strong&gt;. You get a &lt;strong&gt;Board&lt;/strong&gt; (tile) layer for the table surface and a &lt;strong&gt;Pieces&lt;/strong&gt; (entity) layer for the cards. Keeping the felt and the cards on separate layers matters: the felt is static grid data, while the cards are objects your rules deal, flip, and clear. A small grid (here 8×5) is all a card table needs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe2fuzeiybkc3shkq90va.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe2fuzeiybkc3shkq90va.png" alt="A new board scene for the card table" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — Lay the felt
&lt;/h2&gt;

&lt;p&gt;Select the &lt;strong&gt;Board&lt;/strong&gt; layer, pick the green tile from the palette, and paint the whole grid. That green surface is your table; everything else sits on top of it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8eegvaocdalmatut3crl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8eegvaocdalmatut3crl.png" alt="The green felt table" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — Deal the hands
&lt;/h2&gt;

&lt;p&gt;Switch to the &lt;strong&gt;Pieces&lt;/strong&gt; layer and place &lt;strong&gt;Card&lt;/strong&gt; actors: a row for the dealer along the top and a row for Duke along the bottom. Each card carries three properties in the Inspector — &lt;code&gt;rank&lt;/code&gt; (&lt;code&gt;A&lt;/code&gt;, &lt;code&gt;2&lt;/code&gt;…&lt;code&gt;10&lt;/code&gt;, &lt;code&gt;J&lt;/code&gt;, &lt;code&gt;Q&lt;/code&gt;, &lt;code&gt;K&lt;/code&gt;), &lt;code&gt;suit&lt;/code&gt; (Spades / Hearts / Diamonds / Clubs) and &lt;code&gt;faceUp&lt;/code&gt;. Turn the dealer's second card &lt;strong&gt;face-down&lt;/strong&gt; — that's the hole card the player can't see yet.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fktnmswda59d7o44xplg7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fktnmswda59d7o44xplg7.png" alt="The opening blackjack deal" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You &lt;em&gt;can&lt;/em&gt; place cards by hand for a fixed layout, but a real game deals them from a shuffled deck at runtime — your companion creates the card elements from the engine below. The editor just lets you design the table, the card art and the seating; the deal is data your code produces.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 — Hit or stand
&lt;/h2&gt;

&lt;p&gt;Duke's hand here is on 15 (and his Jack already wears the Duke mascot) — too low to stand on. He &lt;strong&gt;hits&lt;/strong&gt;, and a third card brings him to 19, a total worth standing on. The companion spawns the new card element and lays it next to the others; the same &lt;code&gt;faceUp&lt;/code&gt;/&lt;code&gt;rank&lt;/code&gt;/&lt;code&gt;suit&lt;/code&gt; data drives how it draws. The dealer's hole card stays face-down — it's still Duke's turn.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq1mzcs2qybhmf1ingrzu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq1mzcs2qybhmf1ingrzu.png" alt="Duke hits and reaches 19" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the card art comes from
&lt;/h2&gt;

&lt;p&gt;Let's be exact about this, because "the faces are generated" explains nothing. There is &lt;strong&gt;no card generator in the framework&lt;/strong&gt;. Your companion class draws each card itself with the ordinary &lt;code&gt;Graphics&lt;/code&gt; API — the same one you'd use to custom-paint any Codename One component. When a card needs to be shown, your code is handed a &lt;code&gt;Graphics&lt;/code&gt; and a rectangle and decides what goes in it.&lt;/p&gt;

&lt;p&gt;A card element holds just two fields, &lt;code&gt;rank&lt;/code&gt; and &lt;code&gt;suit&lt;/code&gt;. One short method in &lt;em&gt;your&lt;/em&gt; game turns that into a card — this is the actual routine the preview above uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;drawCard&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Graphics&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;suit&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="n"&gt;faceUp&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;faceUp&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;                                  &lt;span class="c1"&gt;// the back: a blue card with the Duke mascot&lt;/span&gt;
        &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setColor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0x1B3A6B&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fillRoundRect&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;drawImage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;duke&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;dw&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;dh&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dw&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dh&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setColor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0xFFFFFF&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;                           &lt;span class="c1"&gt;// the face: a white card...&lt;/span&gt;
    &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fillRoundRect&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setColor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isRed&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;suit&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="mh"&gt;0xC0392B&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;0x1A1A1A&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;drawString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;               &lt;span class="c1"&gt;// ...rank in the corner...&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isFaceCard&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;drawImage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;duke&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;dw&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dw&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dh&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// Duke on the J, Q and K&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;drawSuitPip&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;suit&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// ...or a suit symbol in the middle&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;duke&lt;/code&gt; here is the &lt;strong&gt;Duke mascot&lt;/strong&gt; — the classic waving Duke (the &lt;a href="https://wiki.openjdk.org/display/duke/Main" rel="noopener noreferrer"&gt;BSD-licensed Java mascot&lt;/a&gt;), bundled with the game as an editable image. He rides the card &lt;strong&gt;back&lt;/strong&gt; and every &lt;strong&gt;picture card&lt;/strong&gt; (Jack, Queen, King), which is what actually makes it &lt;em&gt;Duke&lt;/em&gt; Jack. The number cards are a rank label plus a drawn suit shape, so &lt;strong&gt;two pieces of art — the Duke image and the four suit symbols — cover the entire 52-card deck&lt;/strong&gt;; everything else is text the code positions.&lt;/p&gt;

&lt;p&gt;So, to answer "is it an image or is it drawn?" — &lt;strong&gt;both, and your code decides the mix:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Drawn from data (the default).&lt;/strong&gt; Cheapest by far: one method, two images, 52 cards, and you restyle the whole deck by editing that method. To change the back, swap the Duke image or draw something else in the &lt;code&gt;!faceUp&lt;/code&gt; branch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fully painted cards.&lt;/strong&gt; Prefer a hand-illustrated King of Hearts? Import 52 images named by rank and suit and &lt;code&gt;g.drawImage&lt;/code&gt; the matching one — it's the same method with a different branch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Either way it's plain drawing code living in your companion class, not a black box you have to accept as-is.&lt;/p&gt;

&lt;p&gt;And that's the quietly powerful part of building games this way. &lt;code&gt;drawCard&lt;/code&gt; isn't using some games-only drawing layer — it's the very same &lt;code&gt;Graphics&lt;/code&gt; API Codename One has painted every button, label and chart with for fourteen years. A &lt;code&gt;GameView&lt;/code&gt; is an ordinary Codename One &lt;code&gt;Component&lt;/code&gt;, so the entire mature graphics stack works &lt;em&gt;inside&lt;/em&gt; your game: &lt;code&gt;Image.createImage(w, h, argb)&lt;/code&gt; mutable-image buffers you draw a sprite into once and reuse, anti-aliased shapes, system fonts, affine transforms, even the chart and SVG libraries. The new gaming API didn't replace any of that — it plugged into it. So a "new" thing (a card game, a 3D dungeon) is really fourteen years of battle-tested API and tooling pointed at a game scene; the card faces here are just the smallest, clearest example of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules: a blackjack engine
&lt;/h2&gt;

&lt;p&gt;The cards are just data; the &lt;em&gt;game&lt;/em&gt; is the rules that read them. Here is a complete, self-contained blackjack engine — no Codename One dependency, so you can drop it straight into your companion (or unit-test it on its own). The only subtlety in blackjack is the Ace, which is worth 11 unless that would bust the hand, in which case it drops to 1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;handValue&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Card&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;hand&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;aces&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Card&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;hand&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;           &lt;span class="c1"&gt;// face cards 10, Ace 11 (for now)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isAce&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="n"&gt;aces&lt;/span&gt;&lt;span class="o"&gt;++;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;21&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;aces&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;  &lt;span class="c1"&gt;// soften Aces until we stop busting&lt;/span&gt;
        &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;aces&lt;/span&gt;&lt;span class="o"&gt;--;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;hit&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;                   &lt;span class="c1"&gt;// player draws&lt;/span&gt;
    &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;draw&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handValue&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;stand&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// 21 stands, a bust ends the round&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;stand&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;                 &lt;span class="c1"&gt;// player done — dealer plays, then settle&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handValue&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handValue&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dealer&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;dealer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;draw&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;   &lt;span class="c1"&gt;// dealer hits below 17&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;settle&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;settle&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;handValue&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;handValue&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dealer&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;                                   &lt;span class="n"&gt;outcome&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;DEALER_WIN&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// player bust&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isBlackjack&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;isBlackjack&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dealer&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="n"&gt;outcome&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;PLAYER_BLACKJACK&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;21&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;                     &lt;span class="n"&gt;outcome&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;PLAYER_WIN&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// dealer bust / higher&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;                               &lt;span class="n"&gt;outcome&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;DEALER_WIN&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;                                          &lt;span class="n"&gt;outcome&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;PUSH&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;         &lt;span class="c1"&gt;// equal totals&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The deck is a shuffled list of 52 &lt;code&gt;Card&lt;/code&gt;s (&lt;code&gt;rank&lt;/code&gt; 1–13, &lt;code&gt;suit&lt;/code&gt; 0–3); &lt;code&gt;draw()&lt;/code&gt; pops the next one. A two-card 21 is a natural that wins immediately and pays before the dealer draws. That's the whole game — everything else is presentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it into the companion
&lt;/h2&gt;

&lt;p&gt;The companion is the same shape as Tutorial 1 — a &lt;code&gt;GameSceneView&lt;/code&gt; with a generated &lt;code&gt;loadLevel()&lt;/code&gt; and your &lt;code&gt;onUpdate&lt;/code&gt;. You deal in &lt;code&gt;onSetup&lt;/code&gt;, then read taps (or on-screen &lt;strong&gt;Hit&lt;/strong&gt; / &lt;strong&gt;Stand&lt;/strong&gt; buttons) and turn them into engine calls, re-laying the card elements after each move:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Blackjack&lt;/span&gt; &lt;span class="n"&gt;game&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blackjack&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Random&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

&lt;span class="nd"&gt;@Override&lt;/span&gt;
&lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;onUpdate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;deltaSeconds&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;GameInput&lt;/span&gt; &lt;span class="n"&gt;in&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;getInput&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;game&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;phase&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;Blackjack&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Phase&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;PLAYER_TURN&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;in&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;wasPointerPressed&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// tap the top half of the screen to hit, the bottom half to stand&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;in&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPointerY&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;getHeight&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;game&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hit&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;                                    &lt;span class="n"&gt;game&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stand&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;layoutCards&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;                          &lt;span class="c1"&gt;// re-place the card sprites from the hands&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;game&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;phase&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;Blackjack&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Phase&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DONE&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;Dialog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;show&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Duke Jack"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;game&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;resultText&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="s"&gt;"Deal again"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;layoutCards()&lt;/code&gt; is the bridge from rules to pixels: clear the old card sprites and, for each card in &lt;code&gt;game.dealerHand()&lt;/code&gt; and &lt;code&gt;game.playerHand()&lt;/code&gt;, place a card element with that &lt;code&gt;rank&lt;/code&gt;/&lt;code&gt;suit&lt;/code&gt; (and the dealer's hole card face-down until &lt;code&gt;game.phase()&lt;/code&gt; leaves &lt;code&gt;PLAYER_TURN&lt;/code&gt;). Because cards are just elements, dealing, hitting and clearing the table are the same add/remove operations you used for coins in &lt;a href="https://www.codenameone.com/blog/game-builder-2d-platformer/#your-rules-coins-the-slime-and-winning" rel="noopener noreferrer"&gt;Tutorial 1&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Play it
&lt;/h2&gt;

&lt;p&gt;Press &lt;strong&gt;Live&lt;/strong&gt;. The table comes up, Duke stands on his 19, and the dealer turns over the hole card and draws up to 18 — Duke wins the hand:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftw52os07uxlyn51en3wx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftw52os07uxlyn51en3wx.png" alt="Duke wins the hand, 19 over 18" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Dealing a card is just creating its element and sliding it onto the felt:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fivu32w70p6omq4w5uvrn.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fivu32w70p6omq4w5uvrn.gif" alt="Dealing a card in Duke Jack" width="420" height="355"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What got saved, and how it renders
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Save&lt;/strong&gt; writes &lt;code&gt;src/main/resources/games/DukeJack.game&lt;/code&gt; (loaded at runtime as &lt;code&gt;/DukeJack.game&lt;/code&gt; — Codename One's resource namespace is flat). The felt and any cards you placed by hand live there as data; the dealt cards are created by your companion at runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"board"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cols"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"rows"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tileSize"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"layers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Board"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tile"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nl"&gt;"tiles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"0,0"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"1,0"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"start"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Pieces"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"entity"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"elements"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"d1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"assetId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"card"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"layer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Pieces"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"x"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;217&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"y"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;96&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"props"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"rank"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"6"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"suit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hearts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"faceUp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"d2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"assetId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"card"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"layer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Pieces"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"x"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;295&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"y"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;96&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"props"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"rank"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"suit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Spades"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"faceUp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A turn-based card game leans even harder on Codename One's UI than an action game does — and that's exactly where Codename One shines. A running-total label is a &lt;code&gt;Label&lt;/code&gt; in the toolbar; the &lt;strong&gt;Hit&lt;/strong&gt;/&lt;strong&gt;Stand&lt;/strong&gt; controls are two &lt;code&gt;Button&lt;/code&gt;s; the result is a &lt;code&gt;Dialog.show(...)&lt;/code&gt; with a "Deal again" option. As &lt;a href="https://www.codenameone.com/blog/game-builder-2d-platformer/#menus-hud-and-pause" rel="noopener noreferrer"&gt;Tutorial 1's menu section&lt;/a&gt; shows, all of it is the standard UI toolkit wrapped around your &lt;code&gt;GameSceneView&lt;/code&gt; — no separate game-UI layer to learn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Variations and next steps
&lt;/h2&gt;

&lt;p&gt;The card pattern generalizes to most table games:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Betting and a bankroll&lt;/strong&gt; — track chips, pay 3:2 on a natural, and let the player raise before the deal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Split and double-down&lt;/strong&gt; — both are just more engine states over the same hands and card elements.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Other games&lt;/strong&gt; — the &lt;code&gt;Card&lt;/code&gt; element plus a rules class gives you solitaire, poker or war; only the rules change, the table and the deal don't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiplayer&lt;/strong&gt; — deal extra hands as more rows of card elements and loop the turn over them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next: &lt;a href="https://www.codenameone.com/blog/game-builder-3d-dungeon/" rel="noopener noreferrer"&gt;Tutorial 3 — a first-person 3D dungeon&lt;/a&gt;, where the same data drives a 3D &lt;code&gt;GameView&lt;/code&gt; with walls, terrain, lighting, and Duke fighting tea cups with coffee beans.&lt;/p&gt;

</description>
      <category>java</category>
      <category>mobile</category>
      <category>android</category>
      <category>ios</category>
    </item>
    <item>
      <title>Device Integrity And App Review: RASP, Attestation, And Asking For Ratings Right</title>
      <dc:creator>Shai Almog</dc:creator>
      <pubDate>Wed, 08 Jul 2026 15:12:56 +0000</pubDate>
      <link>https://dev.to/codenameone/device-integrity-and-app-review-rasp-attestation-and-asking-for-ratings-right-2n1j</link>
      <guid>https://dev.to/codenameone/device-integrity-and-app-review-rasp-attestation-and-asking-for-ratings-right-2n1j</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6uu4wannkh30iwgz7qdv.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6uu4wannkh30iwgz7qdv.jpg" alt="Device Integrity And App Review: RASP, Attestation, And Asking For Ratings Right" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.codenameone.com/blog/funding-open-source-without-the-bait-and-switch/" rel="noopener noreferrer"&gt;Friday's post on funding open source without the bait-and-switch&lt;/a&gt; covered the model behind this week's releases. Here are two more features from it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is Codename One?&lt;/strong&gt; Codename One is an open-source framework for building native iOS, Android, desktop, and web apps from a single Java or Kotlin codebase. Learn more at &lt;a href="https://www.codenameone.com/" rel="noopener noreferrer"&gt;codenameone.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Both ship this week. Both are small. Both live in core: no cn1lib to add, no native interface for you to write. They follow the same pattern you have seen across Codename One for years. There is a portable Java API, it calls through to native behavior where the platform supports it, and it falls back to something reasonable everywhere else.&lt;/p&gt;

&lt;p&gt;One handles security. The other handles asking users for a rating without annoying them. They are unrelated features that happened to land in the same week, so I am covering them together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Device integrity
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;com.codename1.security.DeviceIntegrity&lt;/code&gt; is a portable runtime self-protection (RASP) and attestation API. It exists for the apps that actually need it: banking, payments, anything where you have to detect a hostile runtime and react to it. We already serve several customers in banking, and Codename One is hardened to meet the requirements they bring; this API is part of that work. If your app is a recipe browser, you can skip this section.&lt;/p&gt;

&lt;p&gt;There are four capabilities. Each one has a zero-code build hint that turns it on, and a runtime API you call when you want to make decisions in Java.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Build hint (zero-code)&lt;/th&gt;
&lt;th&gt;Runtime API&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Play Integrity launch gate&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;android.playIntegrity&lt;/code&gt; (+ &lt;code&gt;.verifyUrl&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;requestIntegrityToken(nonce)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iOS App Attest&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ios.appAttest&lt;/code&gt; (+ &lt;code&gt;.environment&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;requestIntegrityToken(nonce)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RASP root/jailbreak/Frida&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;android.rootCheck&lt;/code&gt;, &lt;code&gt;android.fridaDetection&lt;/code&gt;, &lt;code&gt;ios.detectJailbreak&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;isDeviceCompromised()&lt;/code&gt;, &lt;code&gt;getCompromiseReasons()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accessibility-abuse guard&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;android.accessibilityGuard&lt;/code&gt; (+ &lt;code&gt;.allow&lt;/code&gt;, + &lt;code&gt;.mode&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;getEnabledAccessibilityServices()&lt;/code&gt;, &lt;code&gt;hasUntrustedAccessibilityService(...)&lt;/code&gt;, &lt;code&gt;setSecureScreen(bool)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The design mirrors the existing &lt;code&gt;isJailbrokenDevice()&lt;/code&gt; chain. The API surfaces through &lt;code&gt;Display&lt;/code&gt;, defaults to "unsupported" in &lt;code&gt;CodenameOneImplementation&lt;/code&gt;, and gets overridden in the Android and iOS ports. On Android, Play Integrity and RootBeer are invoked via reflection so the port jar stays dependency-free; the SDK is only on the classpath when the build hint bundles it. On iOS, App Attest goes through &lt;code&gt;DCAppAttestService&lt;/code&gt;, gated behind a &lt;code&gt;CN1_USE_APP_ATTEST&lt;/code&gt; macro with a stub &lt;code&gt;#else&lt;/code&gt; so a build without attestation neither imports nor links &lt;code&gt;DeviceCheck.framework&lt;/code&gt;. The async result comes back through static callbacks, the same shape as &lt;code&gt;IOSBiometrics&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Requesting a token looks like this. The nonce comes from your server, and the token goes straight back to your server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;nonce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;myBackend&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fetchIntegrityNonce&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="nc"&gt;DeviceIntegrity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestIntegrityToken&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;ready&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// token is opaque to the client; only the server can read it&lt;/span&gt;
    &lt;span class="n"&gt;myBackend&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;verifyIntegrity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A cheaper signal, no round trip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DeviceIntegrity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isDeviceCompromised&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;DeviceIntegrity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getCompromiseReasons&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;p&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"RASP signal: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most important sentence in this section: the client is not the security boundary. An attestation token is opaque. The API hands the signed token to your app, but the block-or-allow decision has to happen on your backend, because the verification keys live there and the device does not get a vote. If you treat &lt;code&gt;isDeviceCompromised()&lt;/code&gt; returning &lt;code&gt;false&lt;/code&gt; as proof the device is clean, you have built nothing. A determined attacker who controls the device can tamper with what your app sees. RASP signals are defense-in-depth that raise the cost of an attack. They are not a guarantee!&lt;/p&gt;

&lt;p&gt;The accessibility-abuse guard deserves a note because it is the newest of the four. Overlay and accessibility-service malware is a real vector for reading and driving banking apps. &lt;code&gt;getEnabledAccessibilityServices()&lt;/code&gt; tells you what is active, &lt;code&gt;hasUntrustedAccessibilityService(...)&lt;/code&gt; evaluates that list against an allow-list you configure with &lt;code&gt;android.accessibilityGuard.allow&lt;/code&gt;, and &lt;code&gt;setSecureScreen(true)&lt;/code&gt; blocks screen capture on sensitive forms. The allow-list evaluation is exactly what &lt;code&gt;DeviceIntegrityTest&lt;/code&gt; exercises across its six tests.&lt;/p&gt;

&lt;p&gt;There is also a new security chapter section in the developer guide that walks through wiring the token verification on the server side.&lt;/p&gt;

&lt;h2&gt;
  
  
  App review
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;com.codename1.appreview&lt;/code&gt; solves a smaller problem: asking for a rating at the right moment, through the right channel. It uses the platform's native store-review prompt when one exists, falls back to a built-in Codename One widget where it does not, and leaves the decision of &lt;em&gt;when&lt;/em&gt; to ask entirely up to you.&lt;/p&gt;

&lt;p&gt;There are two ways to use it. The manual style asks whenever your code decides it makes sense:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;AppReview&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;requestReview&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scheduled style lets you set thresholds and then forget about them. You call &lt;code&gt;registerSession()&lt;/code&gt; on every launch, and the scheduler decides whether the moment is right:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;AppReview&lt;/span&gt; &lt;span class="n"&gt;review&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AppReview&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;review&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setMinimumLaunches&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;review&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setMinimumDaysInstalled&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;review&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setDaysBetweenPrompts&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;review&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;registerSession&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;State lives in &lt;code&gt;Preferences&lt;/code&gt;. Once a user rates or opts out, they are never prompted again. That is the whole point of the defaults: they stop you from over-asking.&lt;/p&gt;

&lt;p&gt;The fallback widget does something the native prompts cannot. It splits feedback by sentiment. A high rating (at or above &lt;code&gt;highRatingThreshold&lt;/code&gt;) routes the user to the store. A low rating routes to a pluggable &lt;code&gt;FeedbackListener&lt;/code&gt;, and an email helper ships in the box. So an unhappy user reaches you privately instead of leaving a one-star public review you can do nothing about. The widget itself is &lt;code&gt;RatingDialog&lt;/code&gt;, package-private, stars plus a feedback field.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbgsszvfqldbom9r3mnva.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbgsszvfqldbom9r3mnva.png" alt="The AppReview fallback rating widget showing five stars and a Don't ask again option" width="800" height="182"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The built-in fallback shown on platforms without a native in-app review prompt. On iOS and Android the native sheet is used instead.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here is the decision flow end to end:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRECiAgQVtyZWdpc3RlclNlc3Npb24gb24gbGF1bmNoXSAtLT4gQntUaHJlc2hvbGRzIG1ldD99CiAgQiAtLSBObyAtLT4gWltEbyBub3RoaW5nXQogIEIgLS0gWWVzIC0tPiBDe05hdGl2ZSBpbi1hcHAgcmV2aWV3IHN1cHBvcnRlZD99CiAgQyAtLSBZZXMgLS0-IERbU2hvdyBuYXRpdmUgc3RvcmUgc2hlZXRdCiAgQyAtLSBObyAtLT4gRVtTaG93IFJhdGluZ0RpYWxvZ10KICBFIC0tPiBGe1JhdGluZyA-PSBoaWdoUmF0aW5nVGhyZXNob2xkP30KICBGIC0tIFllcyAtLT4gR1tTZW5kIHVzZXIgdG8gdGhlIHN0b3JlXQogIEYgLS0gTm8gLS0-IEhbUm91dGUgdG8gRmVlZGJhY2tMaXN0ZW5lcl0%3D%3Ftype%3Dpng%26bgColor%3Dffffff" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRECiAgQVtyZWdpc3RlclNlc3Npb24gb24gbGF1bmNoXSAtLT4gQntUaHJlc2hvbGRzIG1ldD99CiAgQiAtLSBObyAtLT4gWltEbyBub3RoaW5nXQogIEIgLS0gWWVzIC0tPiBDe05hdGl2ZSBpbi1hcHAgcmV2aWV3IHN1cHBvcnRlZD99CiAgQyAtLSBZZXMgLS0-IERbU2hvdyBuYXRpdmUgc3RvcmUgc2hlZXRdCiAgQyAtLSBObyAtLT4gRVtTaG93IFJhdGluZ0RpYWxvZ10KICBFIC0tPiBGe1JhdGluZyA-PSBoaWdoUmF0aW5nVGhyZXNob2xkP30KICBGIC0tIFllcyAtLT4gR1tTZW5kIHVzZXIgdG8gdGhlIHN0b3JlXQogIEYgLS0gTm8gLS0-IEhbUm91dGUgdG8gRmVlZGJhY2tMaXN0ZW5lcl0%3D%3Ftype%3Dpng%26bgColor%3Dffffff" alt="Diagram" width="743" height="1229"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The native plumbing mirrors &lt;code&gt;share()&lt;/code&gt; and &lt;code&gt;dial()&lt;/code&gt;. &lt;code&gt;CodenameOneImplementation.isNativeInAppReviewSupported()&lt;/code&gt; and &lt;code&gt;requestNativeInAppReview(SuccessCallback&amp;lt;Boolean&amp;gt;)&lt;/code&gt; are exposed through &lt;code&gt;Display&lt;/code&gt; and &lt;code&gt;CN&lt;/code&gt;. On iOS it calls &lt;code&gt;SKStoreReviewController.requestReview&lt;/code&gt; (iOS 10.3 and up), guarded by a &lt;code&gt;CN1_USE_APPREVIEW&lt;/code&gt; macro. On Android it drives the Play In-App Review flow via reflection, so the port compiles without the extra dependency present.&lt;/p&gt;

&lt;p&gt;The dependency part is what I like most here, because you do not configure it. The Android and iOS builders already scan your compiled classes to auto-enable native deps, and this extends that scan. Merely referencing &lt;code&gt;com.codename1.appreview&lt;/code&gt; (or the &lt;code&gt;CN&lt;/code&gt;/&lt;code&gt;Display&lt;/code&gt; review methods) auto-injects &lt;code&gt;com.google.android.play:review&lt;/code&gt; on Android, and links &lt;code&gt;StoreKit.framework&lt;/code&gt; plus flips &lt;code&gt;CN1_USE_APPREVIEW&lt;/code&gt; on iOS. An app that never asks for a review carries no extra weight. There is no build hint to remember and no checkbox to forget. The scheduler logic that decides all this is covered by eight JUnit 5 tests, and there is a new App-Review chapter in the developer guide.&lt;/p&gt;

&lt;p&gt;The tradeoff to keep in mind: nagging users for ratings backfires. A prompt at the wrong moment costs you a rating and some goodwill. The scheduler defaults and the never-re-prompt rule exist precisely so the easy path is the restrained one. If you fight them by manually firing &lt;code&gt;requestReview()&lt;/code&gt; on every launch, you will earn those one-star reviews.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Two features, same shape. A portable Java API in core, native behavior where the platform offers it, a sane fallback where it does not, and no native interface for you to write. DeviceIntegrity gives high-security apps the attestation and RASP signals they need, as long as you remember the verification belongs on your server. AppReview gets you ratings without turning into the app everyone mutes. Pull the latest build and try them.&lt;/p&gt;

</description>
      <category>java</category>
      <category>mobile</category>
      <category>android</category>
      <category>ios</category>
    </item>
    <item>
      <title>Apple TV And Android TV: One Codebase, And CSS @media For The Living Room</title>
      <dc:creator>Shai Almog</dc:creator>
      <pubDate>Tue, 07 Jul 2026 15:40:57 +0000</pubDate>
      <link>https://dev.to/codenameone/apple-tv-and-android-tv-one-codebase-and-css-media-for-the-living-room-bb0</link>
      <guid>https://dev.to/codenameone/apple-tv-and-android-tv-one-codebase-and-css-media-for-the-living-room-bb0</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8wxwautm0ndg5jepseea.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8wxwautm0ndg5jepseea.jpg" alt="Apple TV And Android TV: One Codebase, And CSS @media For The Living Room" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the Tuesday follow-up to Friday's &lt;a href="https://www.codenameone.com/blog/funding-open-source-without-the-bait-and-switch/" rel="noopener noreferrer"&gt;funding open source without the bait and switch&lt;/a&gt;, and it picks up right where &lt;a href="https://www.codenameone.com/blog/native-apple-watch-and-wear/" rel="noopener noreferrer"&gt;last week's Apple Watch port&lt;/a&gt; left off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is Codename One?&lt;/strong&gt; Codename One is an open-source framework for building native iOS, Android, desktop, and web apps from a single Java or Kotlin codebase. Learn more at &lt;a href="https://www.codenameone.com/" rel="noopener noreferrer"&gt;codenameone.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The watch work gave us the pattern for a new form factor: a shared API to detect it, resource overrides to theme it, and a builder path to ship it. &lt;a href="https://github.com/codenameone/CodenameOne/pull/5261" rel="noopener noreferrer"&gt;PR #5261&lt;/a&gt; applies that same pattern to the television. You can now detect a TV at runtime, restyle for it from CSS, ship a single Android APK that also runs on Google TV, and generate a separate Apple TV target from the same project.&lt;/p&gt;

&lt;p&gt;All of it is driven by the build. You set a flag or two and the builders generate and build the TV targets for you. The screenshots later in this post are real Codename One UI running on the Apple TV simulator, pulled straight from the tvOS screenshot test suite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why TV is its own form factor
&lt;/h2&gt;

&lt;p&gt;A phone and a TV are both screens, but the interaction model is not the same. You sit ten feet away from a TV, so text and focus rings that look fine on a handset become unreadable. There's no touchscreen, so every bit of navigation runs through a remote and a focus cursor. The aspect ratio is fixed and wide. The "ten-foot UI" is a real constraint, not a skin.&lt;/p&gt;

&lt;p&gt;The goal is to keep this one codebase while still respecting those differences. Most of the adaptation is visual, so most of it belongs in your theme rather than in Java branches. A smaller set of behaviors -- the things CSS can't express -- get a runtime check.&lt;/p&gt;

&lt;h2&gt;
  
  
  The form-factor API and the &lt;a class="mentioned-user" href="https://dev.to/media"&gt;@media&lt;/a&gt; styling story
&lt;/h2&gt;

&lt;p&gt;The detection API mirrors the existing &lt;code&gt;isWatch()&lt;/code&gt;. You get &lt;code&gt;CN.isTV()&lt;/code&gt;, &lt;code&gt;Display.isTV()&lt;/code&gt;, and &lt;code&gt;CodenameOneImplementation.isTV()&lt;/code&gt;. On iOS that's backed by a new &lt;code&gt;isRunningOnTV()&lt;/code&gt; native call wired to &lt;code&gt;TARGET_OS_TV&lt;/code&gt;. On Android it checks the television/leanback feature with a &lt;code&gt;UiModeManager&lt;/code&gt; fallback. Resource and theme platform overrides follow the same scheme as the rest of the framework: &lt;code&gt;{tv, ios, appletv}&lt;/code&gt; on iOS and &lt;code&gt;{tv, android, android-tv}&lt;/code&gt; on Android.&lt;/p&gt;

&lt;p&gt;The part with the widest reach is the styling. &lt;code&gt;Resources.loadTheme&lt;/code&gt; now selects &lt;code&gt;device-tv&lt;/code&gt; and &lt;code&gt;device-watch&lt;/code&gt; &lt;code&gt;@media&lt;/code&gt; variants &lt;strong&gt;at runtime&lt;/strong&gt;. So you adapt to the living room or the wrist inside your CSS instead of forking layout code in Java:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Base style for everything */&lt;/span&gt;
&lt;span class="nt"&gt;Title&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3mm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ffffff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1mm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* Ten-foot UI: bigger type, heavier focus treatment */&lt;/span&gt;
&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="n"&gt;device-tv&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nt"&gt;Title&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;9mm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4mm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;Button&lt;/span&gt;&lt;span class="nc"&gt;.selected&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="m"&gt;#ffcc00&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5mm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* The wrist: shrink everything down */&lt;/span&gt;
&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="n"&gt;device-watch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nt"&gt;Title&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2mm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5mm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That same change finally completed &lt;code&gt;@media&lt;/code&gt; for the watch port, which had the override slots but never had the query wired in. Both form factors now read from the same path. It's covered by &lt;code&gt;CSSDeviceFormFactorMediaQueryTest&lt;/code&gt;, which runs against the real CSS compiler rather than a mock, and it's documented in &lt;code&gt;css.asciidoc&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For the cases CSS can't reach -- behavior, not appearance -- you branch in Java:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Form&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Form&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Home"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;BoxLayout&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;CN&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isTV&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// No touch: make the first item focused so the remote has a target&lt;/span&gt;
    &lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Watch Now"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addShowListener&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestFocus&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Watch Now"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;show&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep these branches small. Anything that's purely a size or color difference should live in the &lt;code&gt;@media&lt;/code&gt; block above, not here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Android TV: one APK, one build hint
&lt;/h2&gt;

&lt;p&gt;Android TV is the low-code switch. You don't build a separate artifact. The same APK runs on phones, tablets, and the TV. You turn it on with a build hint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# Build hints
&lt;/span&gt;&lt;span class="py"&gt;android.tv&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;android.tv.banner&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;tv_banner.png&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;android.tv&lt;/code&gt; is set, &lt;code&gt;AndroidGradleBuilder&lt;/code&gt; adds the &lt;code&gt;LEANBACK_LAUNCHER&lt;/code&gt; intent category so the app shows up on the TV home screen, declares the &lt;code&gt;android.software.leanback&lt;/code&gt; uses-feature, marks the touchscreen optional so non-touch devices still qualify, and generates a 320×180 &lt;code&gt;tv_banner&lt;/code&gt; (overridable via &lt;code&gt;android.tv.banner&lt;/code&gt;). One build, three device classes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apple TV: a separate Metal target
&lt;/h2&gt;

&lt;p&gt;Apple TV can't ride the same binary. tvOS needs its own Xcode target, the same way Mac Catalyst does. The new &lt;code&gt;TvNativeBuilder&lt;/code&gt; adds an &lt;code&gt;appletvos&lt;/code&gt; target with &lt;code&gt;TARGETED_DEVICE_FAMILY=3&lt;/code&gt;, renders through Metal, and excludes the OpenGL-only &lt;code&gt;.m&lt;/code&gt; files (tvOS has Metal and most of the iOS APIs, but no OpenGL ES). It reuses the shared &lt;code&gt;UIApplicationMain&lt;/code&gt; entry point and is modeled directly on &lt;code&gt;MacNativeBuilder&lt;/code&gt;. It's wired into &lt;code&gt;IPhoneBuilder&lt;/code&gt;, the build mojo via the &lt;code&gt;codename1.tvMain&lt;/code&gt; build property, and the build-hint schema.&lt;/p&gt;

&lt;p&gt;Here's how one source maps to the three outputs:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRECiAgU1JDWyJZb3VyIEphdmEvS290bGluIHNvdXJjZTxici8-KyB0aGVtZSBDU1MiXQogIEZGWyJTaGFyZWQgZm9ybS1mYWN0b3IgbGF5ZXI8YnIvPkNOLmlzVFYoKSAvIGlzV2F0Y2goKTxici8-ZGV2aWNlLXR2IC8gZGV2aWNlLXdhdGNoIEBtZWRpYSJdCiAgU1JDIC0tPiBGRgoKICBGRiAtLT4gQU5EUk9JRFsiQW5kcm9pZCBidWlsZCJdCiAgRkYgLS0-IElPU1siaU9TIGJ1aWxkIl0KCiAgQU5EUk9JRCAtLT4gQVBLWyJTaW5nbGUgQVBLPGJyLz5hbmRyb2lkLnR2IGhpbnQgYWRkczxici8-TEVBTkJBQ0sgKyBsZWFuYmFjayBmZWF0dXJlPGJyLz4rIHR2X2Jhbm5lciJdCiAgQVBLIC0tPiBQSE9ORVsiUGhvbmUgLyBUYWJsZXQiXQogIEFQSyAtLT4gR1RWWyJHb29nbGUgVFYiXQoKICBJT1MgLS0-IElQSE9ORVsiaVBob25lIC8gaVBhZCB0YXJnZXQiXQogIElPUyAtLT4gVFZUQVJHRVRbImFwcGxldHZvcyB0YXJnZXQ8YnIvPlR2TmF0aXZlQnVpbGRlciArIGNvZGVuYW1lMS50dk1haW48YnIvPlRBUkdFVEVEX0RFVklDRV9GQU1JTFk9MywgTWV0YWwiXQogIFRWVEFSR0VUIC0tPiBBVFZbIkFwcGxlIFRWIl0%3D%3Ftype%3Dpng%26bgColor%3Dffffff" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRECiAgU1JDWyJZb3VyIEphdmEvS290bGluIHNvdXJjZTxici8-KyB0aGVtZSBDU1MiXQogIEZGWyJTaGFyZWQgZm9ybS1mYWN0b3IgbGF5ZXI8YnIvPkNOLmlzVFYoKSAvIGlzV2F0Y2goKTxici8-ZGV2aWNlLXR2IC8gZGV2aWNlLXdhdGNoIEBtZWRpYSJdCiAgU1JDIC0tPiBGRgoKICBGRiAtLT4gQU5EUk9JRFsiQW5kcm9pZCBidWlsZCJdCiAgRkYgLS0-IElPU1siaU9TIGJ1aWxkIl0KCiAgQU5EUk9JRCAtLT4gQVBLWyJTaW5nbGUgQVBLPGJyLz5hbmRyb2lkLnR2IGhpbnQgYWRkczxici8-TEVBTkJBQ0sgKyBsZWFuYmFjayBmZWF0dXJlPGJyLz4rIHR2X2Jhbm5lciJdCiAgQVBLIC0tPiBQSE9ORVsiUGhvbmUgLyBUYWJsZXQiXQogIEFQSyAtLT4gR1RWWyJHb29nbGUgVFYiXQoKICBJT1MgLS0-IElQSE9ORVsiaVBob25lIC8gaVBhZCB0YXJnZXQiXQogIElPUyAtLT4gVFZUQVJHRVRbImFwcGxldHZvcyB0YXJnZXQ8YnIvPlR2TmF0aXZlQnVpbGRlciArIGNvZGVuYW1lMS50dk1haW48YnIvPlRBUkdFVEVEX0RFVklDRV9GQU1JTFk9MywgTWV0YWwiXQogIFRWVEFSR0VUIC0tPiBBVFZbIkFwcGxlIFRWIl0%3D%3Ftype%3Dpng%26bgColor%3Dffffff" alt="Diagram" width="933" height="678"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I built the tvOS target end-to-end against the Xcode 26 / tvOS 26 SDK, and the framework's screenshot suite runs the full component set on the Apple TV simulator. Here is a &lt;code&gt;ChatView&lt;/code&gt; screen rendering at the Apple TV's native 4K, the same Codename One UI the framework draws on every other target:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9enm99503atwcpr6r7mm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9enm99503atwcpr6r7mm.png" alt="A Codename One ChatView screen rendering on the Apple TV simulator at 4K, drawn by the framework's Metal renderer" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning it on
&lt;/h2&gt;

&lt;p&gt;There is no separate SDK to learn. On Android you set &lt;code&gt;android.tv=true&lt;/code&gt; and the same APK gains a TV launcher; on iOS you point the build at the tvOS target with &lt;code&gt;codename1.tvMain&lt;/code&gt; and &lt;code&gt;TvNativeBuilder&lt;/code&gt; generates the &lt;code&gt;appletvos&lt;/code&gt; target. Then you restyle for the ten-foot screen with an &lt;code&gt;@media device-tv&lt;/code&gt; block and branch the few behaviors CSS can't reach on &lt;code&gt;CN.isTV()&lt;/code&gt;. The same project, the same code, three more screens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;The television is now a Codename One form factor, detected with &lt;code&gt;CN.isTV()&lt;/code&gt;, themed with &lt;code&gt;@media device-tv&lt;/code&gt;, and built with a flag. Set &lt;code&gt;android.tv=true&lt;/code&gt; for Google TV and point &lt;code&gt;codename1.tvMain&lt;/code&gt; at your tvOS target for Apple TV, and the screens you already wrote show up in the living room.&lt;/p&gt;

</description>
      <category>java</category>
      <category>mobile</category>
      <category>android</category>
      <category>ios</category>
    </item>
    <item>
      <title>A Privacy-First Analytics API: One Facade, A Provider SPI, And A Consent Gate</title>
      <dc:creator>Shai Almog</dc:creator>
      <pubDate>Mon, 06 Jul 2026 16:14:46 +0000</pubDate>
      <link>https://dev.to/codenameone/a-privacy-first-analytics-api-one-facade-a-provider-spi-and-a-consent-gate-2l1l</link>
      <guid>https://dev.to/codenameone/a-privacy-first-analytics-api-one-facade-a-provider-spi-and-a-consent-gate-2l1l</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbgzzn2kya5lx7x3z8563.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbgzzn2kya5lx7x3z8563.jpg" alt="A Privacy-First Analytics API: One Facade, A Provider SPI, And A Consent Gate" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On Friday I wrote about &lt;a href="https://www.codenameone.com/blog/funding-open-source-without-the-bait-and-switch/" rel="noopener noreferrer"&gt;funding open source without the bait and switch&lt;/a&gt;, and the analytics API in &lt;a href="https://github.com/codenameone/CodenameOne/pull/5266" rel="noopener noreferrer"&gt;PR #5266&lt;/a&gt; is the clearest example of the model in that post: an open seam anyone can plug into, with our paid implementation as one optional choice. This is the Monday follow-up that gets into how the API actually works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is Codename One?&lt;/strong&gt; Codename One is an open-source framework for building native iOS, Android, desktop, and web apps from a single Java or Kotlin codebase. Learn more at &lt;a href="https://www.codenameone.com/" rel="noopener noreferrer"&gt;codenameone.com&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with the old service
&lt;/h2&gt;

&lt;p&gt;Codename One shipped a single &lt;code&gt;AnalyticsService&lt;/code&gt; wired to Google Analytics v1. You called &lt;code&gt;AnalyticsService.init&lt;/code&gt; with your tracking id, and that was the whole story. One vendor, one protocol, baked into the framework.&lt;/p&gt;

&lt;p&gt;That design has aged badly for two reasons. Google Analytics v1 itself is gone, so the integration points at a dead endpoint. And the API has no consent story at all. It starts reporting the moment you initialize it, which is exactly the behavior GDPR and CCPA make untenable. You cannot retrofit "ask the user first" onto an API whose only mode is "send everything now."&lt;/p&gt;

&lt;p&gt;So instead of patching the old service, the new code separates the three concerns that were tangled together: what you report, where it goes, and whether you are allowed to send it.&lt;/p&gt;

&lt;h2&gt;
  
  
  One facade, a provider SPI, a consent gate that runs first
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Analytics&lt;/code&gt; is the facade your app calls. You register one or more &lt;code&gt;AnalyticsProvider&lt;/code&gt; implementations against it, and it fans every &lt;code&gt;screen&lt;/code&gt;, &lt;code&gt;event&lt;/code&gt;, &lt;code&gt;setUserProperty&lt;/code&gt; and &lt;code&gt;crash&lt;/code&gt; call out to all of them. The important word is &lt;em&gt;after&lt;/em&gt;: every reporting call passes through a consent check before any provider sees it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRECiAgQVsiWW91ciBhcHAgY2FsbHMgQW5hbHl0aWNzLmV2ZW50KCkiXSAtLT4gQnsiQ29uc2VudCBnYXRlPGJyLz5vcHQtaW4gYnkgZGVmYXVsdCJ9CiAgQiAtLT58Z3JhbnRlZHwgQ1siZmFuIG91dCB0byBldmVyeSByZWdpc3RlcmVkIHByb3ZpZGVyIl0KICBCIC0tPnxkZW5pZWR8IFpbIm5vdGhpbmcgbGVhdmVzIHRoZSBkZXZpY2UiXQogIEMgLS0-IERbIkdvb2dsZUFuYWx5dGljc1Byb3ZpZGVyIChHQTQpIl0KICBDIC0tPiBFWyJNYXRvbW9BbmFseXRpY3NQcm92aWRlciJdCiAgQyAtLT4gRlsiRmlyZWJhc2VBbmFseXRpY3NQcm92aWRlciJdCiAgQyAtLT4gR1siQ29kZW5hbWVPbmVBbmFseXRpY3NQcm92aWRlciJdCiAgc3R5bGUgQiBmaWxsOiMxZjZmZWIsY29sb3I6I2ZmZgogIHN0eWxlIFogZmlsbDojNmU3NjgxLGNvbG9yOiNmZmY%3D%3Ftype%3Dpng%26bgColor%3Dffffff" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRECiAgQVsiWW91ciBhcHAgY2FsbHMgQW5hbHl0aWNzLmV2ZW50KCkiXSAtLT4gQnsiQ29uc2VudCBnYXRlPGJyLz5vcHQtaW4gYnkgZGVmYXVsdCJ9CiAgQiAtLT58Z3JhbnRlZHwgQ1siZmFuIG91dCB0byBldmVyeSByZWdpc3RlcmVkIHByb3ZpZGVyIl0KICBCIC0tPnxkZW5pZWR8IFpbIm5vdGhpbmcgbGVhdmVzIHRoZSBkZXZpY2UiXQogIEMgLS0-IERbIkdvb2dsZUFuYWx5dGljc1Byb3ZpZGVyIChHQTQpIl0KICBDIC0tPiBFWyJNYXRvbW9BbmFseXRpY3NQcm92aWRlciJdCiAgQyAtLT4gRlsiRmlyZWJhc2VBbmFseXRpY3NQcm92aWRlciJdCiAgQyAtLT4gR1siQ29kZW5hbWVPbmVBbmFseXRpY3NQcm92aWRlciJdCiAgc3R5bGUgQiBmaWxsOiMxZjZmZWIsY29sb3I6I2ZmZgogIHN0eWxlIFogZmlsbDojNmU3NjgxLGNvbG9yOiNmZmY%3D%3Ftype%3Dpng%26bgColor%3Dffffff" alt="Diagram" width="1205" height="616"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The gate is opt-in by default. Until the user grants the matching category, the facade silently drops reporting calls and no provider is invoked. The consent choice and the pseudonymous client id both persist in &lt;code&gt;Preferences&lt;/code&gt;, so they survive restarts. The client id is not derived from any hardware identifier, and &lt;code&gt;resetClientId()&lt;/code&gt; issues a fresh one to honor an erasure request.&lt;/p&gt;

&lt;p&gt;Here is the consent-first ordering in code. Note that the provider is registered, but nothing is reported until consent arrives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Register providers up front -- still silent at this point.&lt;/span&gt;
&lt;span class="nc"&gt;Analytics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addProvider&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MatomoAnalyticsProvider&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://stats.example.com"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// Opt-in is the default: this call is dropped because no consent is recorded yet.&lt;/span&gt;
&lt;span class="nc"&gt;Analytics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;screen&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Home"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// The user accepts your privacy prompt. Grant the categories you asked for.&lt;/span&gt;
&lt;span class="nc"&gt;Analytics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setConsent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AnalyticsConsent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;analytics&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;crashReporting&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

&lt;span class="c1"&gt;// Now reporting flows to every registered provider.&lt;/span&gt;
&lt;span class="nc"&gt;Analytics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;screen&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Home"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;Analytics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AnalyticsEvent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"purchase"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"commerce"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;param&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sku"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"abc-123"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;param&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"value"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;9.99&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Consent is granular, not a single on/off switch. A user can allow crash reporting while declining behavioral analytics, and the facade honors each category independently. &lt;code&gt;AnalyticsConsent.all()&lt;/code&gt; unblocks everything in one line if your app already collected consent through its own prompt or a consent-management platform. If the user later asks to be forgotten:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Analytics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;resetClientId&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// new pseudonymous id, providers re-initialized&lt;/span&gt;
&lt;span class="nc"&gt;Analytics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setConsent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AnalyticsConsent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;none&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The five providers, and mixing them
&lt;/h2&gt;

&lt;p&gt;Five providers ship in the &lt;code&gt;com.codename1.analytics&lt;/code&gt; package:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CodenameOneAnalyticsProvider&lt;/code&gt; batches to the Codename One cloud; it is included with every paid subscription, and the tier sets your data-retention window.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GoogleAnalyticsProvider&lt;/code&gt; speaks the GA4 Measurement Protocol.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MatomoAnalyticsProvider&lt;/code&gt; targets a self-hosted, non-Google backend.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FirebaseAnalyticsProvider&lt;/code&gt; routes through the native Firebase SDK (Android via reflection, iOS via dynamic dispatch on &lt;code&gt;FIRAnalytics&lt;/code&gt;); the builders inject the Firebase dependency only when you set the &lt;code&gt;android.firebaseAnalytics&lt;/code&gt; / &lt;code&gt;ios.firebaseAnalytics&lt;/code&gt; build hints.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LoggingAnalyticsProvider&lt;/code&gt; prints to the log for development.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because the facade fans out to every registered provider, mixing them is just two &lt;code&gt;addProvider&lt;/code&gt; calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Analytics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addProvider&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LoggingAnalyticsProvider&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;          &lt;span class="c1"&gt;// see it locally&lt;/span&gt;
&lt;span class="nc"&gt;Analytics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addProvider&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GoogleAnalyticsProvider&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"G-XXXX"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"api-secret"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Writing your own backend is small. Extend &lt;code&gt;AbstractAnalyticsProvider&lt;/code&gt;, which gives every SPI method an empty body, and override only what you support:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CountingProvider&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractAnalyticsProvider&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"counting"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;trackEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AnalyticsEvent&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="o"&gt;++;&lt;/span&gt;
        &lt;span class="c1"&gt;// event.getName(), event.getCategory(), event.getParameters() are all here.&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;supports&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AnalyticsCapability&lt;/span&gt; &lt;span class="n"&gt;capability&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;capability&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;AnalyticsCapability&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;EVENTS&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;AnalyticsCapability&lt;/code&gt; lets a caller ask a provider what it actually honors (&lt;code&gt;SCREEN_VIEWS&lt;/code&gt;, &lt;code&gt;EVENTS&lt;/code&gt;, &lt;code&gt;CRASH_REPORTING&lt;/code&gt;, &lt;code&gt;FUNNELS&lt;/code&gt;, &lt;code&gt;RAW_EXPORT&lt;/code&gt;, among others) instead of guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first-party backend: events, goals, and a console
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;CodenameOneAnalyticsProvider&lt;/code&gt; is the one provider that needs no third-party account. It batches events to the Build Cloud, where they show up in an analytics console next to your builds. An app appears on its own once it sends data, so there is no dashboard to provision first.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx62vrj2a9q6to6192lns.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx62vrj2a9q6to6192lns.png" alt="The Codename One analytics console Overview, with active-user, form-view and event counts, a form-views-over-time chart, and a left-hand nav for Trends, Forms, Events, Segments, User flow, Goals and Reports" width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The console is more than a raw event log. The Overview gives you headline numbers, active users, form views and events, over a 7-, 30- or 90-day window, and the left nav opens Trends, Forms, Events, Segments, User flow, Goals, and Reports. Common events are understood out of the box: screen and form views are tracked for you, and a &lt;code&gt;purchase&lt;/code&gt; event carrying a &lt;code&gt;value&lt;/code&gt; parameter feeds revenue and funnel reporting without a custom dashboard. Goals let you mark the events that matter and track conversion against them, while the segment and user-flow views answer more involved questions than a flat counter can. All of it is in the console today.&lt;/p&gt;

&lt;p&gt;An app only shows up after it sends data, which only happens after the on-device consent gate opens:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkzihdjmidg4pmgowmhx4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkzihdjmidg4pmgowmhx4.png" alt="The analytics console empty state, explaining that an app appears once it registers CodenameOneAnalyticsProvider and the user grants opt-in analytics consent on the device" width="800" height="357"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first-party provider is included with every paid Codename One subscription, down to the basic tier. What your plan changes is how long your data is retained, not whether you get analytics at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrating from the old service
&lt;/h2&gt;

&lt;p&gt;The old &lt;code&gt;AnalyticsService&lt;/code&gt; still works. It is deprecated, and it now delegates to the new API rather than carrying its own Google Analytics code, so existing apps keep running without a change. The &lt;code&gt;UIBuilder&lt;/code&gt; analytics hook is unchanged too.&lt;/p&gt;

&lt;p&gt;One detail you need to know: &lt;code&gt;AnalyticsService.init()&lt;/code&gt; flips the facade to opt-&lt;em&gt;out&lt;/em&gt; mode. The legacy API always reported immediately, and silently switching it to opt-in would have stopped data flowing for every app that relied on it. So legacy callers keep their old always-on behavior, while new code written against &lt;code&gt;Analytics&lt;/code&gt; gets opt-in by default. Do not assume the new default applies to a project still calling the deprecated service.&lt;/p&gt;

&lt;h2&gt;
  
  
  The business-model thread
&lt;/h2&gt;

&lt;p&gt;This is the shape every paid Codename One service takes, and &lt;a href="https://www.codenameone.com/blog/funding-open-source-without-the-bait-and-switch/" rel="noopener noreferrer"&gt;Friday's post&lt;/a&gt; is the longer argument for why. The analytics API is an open SPI in a framework that stays open, licensed under GPL with the Classpath Exception: point it at Matomo, at GA4, at Firebase, or at a provider you wrote in twenty lines, and the framework does not care. &lt;code&gt;CodenameOneAnalyticsProvider&lt;/code&gt; is one optional implementation of that contract, batched to our cloud and included with every paid subscription down to the basic tier, and choosing it is what helps fund the next port and the next API. It is a better default for people who want consent handling done for them, not a toll gate on the open framework. If you would rather self-host Matomo, you lose nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  A tradeoff worth saying out loud
&lt;/h2&gt;

&lt;p&gt;A consent gate that is off by default means you will measure less than an always-on tracker does. Some users will decline, and their sessions will not show up in your reports. That is the deliberate cost of the privacy posture, and if you need every event you should expect a gap versus the old behavior. The mechanisms here are concrete and limited: opt-in by default, a pseudonymous resettable client id, no hardware identifiers, and an on-device gate that runs before any provider. That is what the API gives you, and it is all it claims to give you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;The new analytics API is one facade, a provider SPI, and a consent gate that runs first. It ships with five providers, keeps the deprecated service working through delegation, and is covered by 25 unit tests across consent gating, the providers, client id handling and the legacy facade. Pick the backend that fits your privacy stance, register it, and ask for consent before you report.&lt;/p&gt;

</description>
      <category>java</category>
      <category>mobile</category>
      <category>android</category>
      <category>ios</category>
    </item>
    <item>
      <title>Rich Text And Code Editing: RichTextArea And A Syntax-Highlighting CodeEditor</title>
      <dc:creator>Shai Almog</dc:creator>
      <pubDate>Sun, 05 Jul 2026 14:34:58 +0000</pubDate>
      <link>https://dev.to/codenameone/rich-text-and-code-editing-richtextarea-and-a-syntax-highlighting-codeeditor-4c82</link>
      <guid>https://dev.to/codenameone/rich-text-and-code-editing-richtextarea-and-a-syntax-highlighting-codeeditor-4c82</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvjezw2eryi1qky4q43tp.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvjezw2eryi1qky4q43tp.jpg" alt="Rich Text And Code Editing: RichTextArea And A Syntax-Highlighting CodeEditor" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.codenameone.com/blog/funding-open-source-without-the-bait-and-switch/" rel="noopener noreferrer"&gt;Friday's post&lt;/a&gt; covered how we fund this work without the bait-and-switch; today is the engineering side of the same week, with two new visual editors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is Codename One?&lt;/strong&gt; Codename One is an open-source framework for building native iOS, Android, desktop, and web apps from a single Java or Kotlin codebase. Learn more at &lt;a href="https://www.codenameone.com/" rel="noopener noreferrer"&gt;codenameone.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;PR #5272 adds two components: &lt;code&gt;RichTextArea&lt;/code&gt;, a WYSIWYG rich text editor, and &lt;code&gt;CodeEditor&lt;/code&gt;, an IDE-style code editor with syntax highlighting. They are different on the surface and identical underneath, which is the part worth explaining first.&lt;/p&gt;

&lt;h2&gt;
  
  
  One abstraction, two backends
&lt;/h2&gt;

&lt;p&gt;Both editors extend &lt;code&gt;AbstractEditorComponent&lt;/code&gt;. That base class does not draw anything itself. It speaks a small semantic channel of commands ("apply bold", "set language to java"), queries ("give me the current HTML") and events ("the text changed") to whatever backend is attached. The component never assumes how the editor is rendered, only what it can ask for and what it gets told back.&lt;/p&gt;

&lt;p&gt;There are two backends behind that channel, and they are interchangeable.&lt;/p&gt;

&lt;p&gt;The first is a cross-platform engine built on &lt;code&gt;BrowserComponent&lt;/code&gt;. The editor's HTML and JavaScript ship inside the core jar, so there is nothing to download at runtime. It renders through whatever web view the platform already provides: &lt;code&gt;WKWebView&lt;/code&gt; on iOS, &lt;code&gt;WebView&lt;/code&gt; on Android, CEF on the desktop simulator, and an iframe on the web target. Going through a real web view means the editor gets correct virtual-keyboard behavior and physical-keyboard handling for free, instead of us re-implementing text input per platform.&lt;/p&gt;

&lt;p&gt;The second backend is optional and supplied by a port. A platform implementation can return a native editor peer through &lt;code&gt;CodenameOneImplementation#createNativeEditorPeer&lt;/code&gt;, then handle &lt;code&gt;editorPeerCommand&lt;/code&gt; and &lt;code&gt;editorPeerQuery&lt;/code&gt;. The same semantic channel drives it, so the component code does not change. &lt;code&gt;CodeEditor#setEngineURL(...)&lt;/code&gt; is the application-level version of the same idea: point the editor at a richer engine, such as a full Monaco or CodeMirror build you host yourself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRECiAgQVsiUmljaFRleHRBcmVhIC8gQ29kZUVkaXRvciJdIC0tPiBCWyJBYnN0cmFjdEVkaXRvckNvbXBvbmVudDxici8-KGNvbW1hbmQgLyBxdWVyeSAvIGV2ZW50IGNoYW5uZWwpIl0KICBCIC0tPiBDWyJDcm9zcy1wbGF0Zm9ybSBlbmdpbmU8YnIvPkJyb3dzZXJDb21wb25lbnQgKyBidW5kbGVkIEhUTUwvSlMiXQogIEIgLS0-IERbIk9wdGlvbmFsIG5hdGl2ZSBwZWVyPGJyLz5jcmVhdGVOYXRpdmVFZGl0b3JQZWVyIC88YnIvPmVkaXRvclBlZXJDb21tYW5kIC8gZWRpdG9yUGVlclF1ZXJ5Il0KICBDIC0tPiBFWyJXS1dlYlZpZXcgKGlPUyk8YnIvPldlYlZpZXcgKEFuZHJvaWQpPGJyLz5DRUYgKGRlc2t0b3ApPGJyLz5pZnJhbWUgKHdlYikiXQ%3D%3D%3Ftype%3Dpng%26bgColor%3Dffffff" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRECiAgQVsiUmljaFRleHRBcmVhIC8gQ29kZUVkaXRvciJdIC0tPiBCWyJBYnN0cmFjdEVkaXRvckNvbXBvbmVudDxici8-KGNvbW1hbmQgLyBxdWVyeSAvIGV2ZW50IGNoYW5uZWwpIl0KICBCIC0tPiBDWyJDcm9zcy1wbGF0Zm9ybSBlbmdpbmU8YnIvPkJyb3dzZXJDb21wb25lbnQgKyBidW5kbGVkIEhUTUwvSlMiXQogIEIgLS0-IERbIk9wdGlvbmFsIG5hdGl2ZSBwZWVyPGJyLz5jcmVhdGVOYXRpdmVFZGl0b3JQZWVyIC88YnIvPmVkaXRvclBlZXJDb21tYW5kIC8gZWRpdG9yUGVlclF1ZXJ5Il0KICBDIC0tPiBFWyJXS1dlYlZpZXcgKGlPUyk8YnIvPldlYlZpZXcgKEFuZHJvaWQpPGJyLz5DRUYgKGRlc2t0b3ApPGJyLz5pZnJhbWUgKHdlYikiXQ%3D%3D%3Ftype%3Dpng%26bgColor%3Dffffff" alt="Diagram" width="586" height="574"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  CodeEditor
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;CodeEditor&lt;/code&gt; highlights eight languages: java, kotlin, js, python, css, xml, json, and c. It draws a line-number gutter, ships light and dark themes, and auto-closes brackets and quotes as you type.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;CodeEditor&lt;/span&gt; &lt;span class="n"&gt;editor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CodeEditor&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;editor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setLanguage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"java"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;editor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setTheme&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dark"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;editor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setText&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"public class Main {\n\n}"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="nc"&gt;Form&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Form&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Editor"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BorderLayout&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BorderLayout&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;CENTER&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;editor&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;show&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code completion is asynchronous, which matters because a real completion source may have to look something up. You register a &lt;code&gt;CodeCompletionProvider&lt;/code&gt;, and it hands results back through a callback rather than returning them inline, so the editor never blocks while you compute suggestions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;editor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setCompletionProvider&lt;/span&gt;&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="n"&gt;ed&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cursorPosition&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Inspect code up to cursorPosition, build suggestions, then deliver.&lt;/span&gt;
    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CodeCompletion&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ArrayList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CodeCompletion&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"println"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"println(String x)"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CodeCompletion&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"print"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"print(String x)"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="c1"&gt;// Hand them back when ready, on or off the EDT (onSucess is CN1's spelling).&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onSucess&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Diagnostics work the same way. You push &lt;code&gt;CodeDiagnostic&lt;/code&gt; entries at the editor and it renders them as squiggly underlines, gutter markers, and tooltips on hover:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;editor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setDiagnostics&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Arrays&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;asList&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CodeDiagnostic&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"cannot find symbol: prinln"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setSeverity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CodeDiagnostic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ERROR&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is the editor with Java highlighting, the gutter, and the completion popup offering &lt;code&gt;println&lt;/code&gt; and &lt;code&gt;print&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvwejeb1wrso42ihf518d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvwejeb1wrso42ihf518d.png" alt="CodeEditor showing Java syntax highlighting, a line-number gutter, and an async code-completion popup with println and print suggestions" width="800" height="1171"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The dark theme is a one-line switch, not a separate component:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyshm7injrna7wjzja2tn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyshm7injrna7wjzja2tn.png" alt="CodeEditor rendered in its dark theme" width="800" height="1171"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  RichTextArea
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;RichTextArea&lt;/code&gt; is the WYSIWYG side. It edits formatted text and gives you HTML in and out. You get bold, italic and underline, ordered and unordered lists, links, foreground and highlight colors, and headings. It fires change events as the user edits, and it shows a placeholder when empty.&lt;/p&gt;

&lt;p&gt;The content model is HTML, so you load and save with two calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;RichTextArea&lt;/span&gt; &lt;span class="n"&gt;rich&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RichTextArea&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;rich&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setPlaceholder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Trip notes..."&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;rich&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setHtml&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;p&amp;gt;Itinerary: &amp;lt;b&amp;gt;Day 1&amp;lt;/b&amp;gt; arrive, see "&lt;/span&gt;
    &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"&amp;lt;a href=\"https://example.com\"&amp;gt;the museum&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;rich&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addChangeListener&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;rich&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getHtml&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// getHtml is async; the markup arrives in the callback.&lt;/span&gt;
    &lt;span class="nc"&gt;Storage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;writeObject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"note"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The toolbar drives the same command channel described earlier; pressing the bold button sends an "apply bold" command to the backend rather than editing a string directly:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdkhuyvz00dtu7xo1kuef.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdkhuyvz00dtu7xo1kuef.png" alt="RichTextArea WYSIWYG editor with a formatting toolbar (bold, italic, underline, lists, link, color) editing a trip itinerary note that contains bold text, a link, and strikethrough" width="800" height="1171"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  You only pay for it if you use it
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;CodeEditor&lt;/code&gt; can be backed by a heavier engine, and CodeMirror assets are not free weight to carry around. So the Android and iOS builders scan your app for use of &lt;code&gt;com.codename1.ui.CodeEditor&lt;/code&gt;. The optional CodeMirror assets are bundled only when that API actually appears, gated by &lt;code&gt;CN1_USE_CODEMIRROR&lt;/code&gt;. An app that never touches &lt;code&gt;CodeEditor&lt;/code&gt; ships nothing extra.&lt;/p&gt;

&lt;p&gt;The Playground already uses &lt;code&gt;CodeEditor&lt;/code&gt; for its editor pane, so it is dogfooded rather than demo-ware. There are 33 deterministic unit tests across &lt;code&gt;RichTextAreaTest&lt;/code&gt; and &lt;code&gt;CodeEditorTest&lt;/code&gt;, backed by an editor SPI in the test implementation so they run without a real web view, plus new developer-guide sections with screenshots.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tradeoff
&lt;/h2&gt;

&lt;p&gt;The default engine is web-view-backed. That is what makes it portable, and it is also the limit: it needs a web view to exist on the platform. iOS, Android, the desktop and the web all have one, so all four are covered. Apple Watch and tvOS do not have a usable web view, so the editors are not enabled there. If a port supplies a native editor peer, that constraint goes away for that port, which is exactly why the second backend exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Two editors, one abstraction. The shared &lt;code&gt;AbstractEditorComponent&lt;/code&gt; is the reason a WYSIWYG HTML editor and a syntax-highlighting code editor could land in the same PR: the hard part was the command/query/event channel and the cross-platform engine behind it, and both components reuse it. The web-view engine gives you something that works everywhere a web view exists, and the native peer hook gives a port room to do better when it can. Try &lt;code&gt;CodeEditor&lt;/code&gt; and &lt;code&gt;RichTextArea&lt;/code&gt; in the next build and tell us where they fall short.&lt;/p&gt;

</description>
      <category>java</category>
      <category>mobile</category>
      <category>android</category>
      <category>ios</category>
    </item>
    <item>
      <title>Maps You Control: A Pure-Vector MapView And Pluggable Native Providers</title>
      <dc:creator>Shai Almog</dc:creator>
      <pubDate>Sat, 04 Jul 2026 14:24:26 +0000</pubDate>
      <link>https://dev.to/codenameone/maps-you-control-a-pure-vector-mapview-and-pluggable-native-providers-409h</link>
      <guid>https://dev.to/codenameone/maps-you-control-a-pure-vector-mapview-and-pluggable-native-providers-409h</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1cl8lwpikftj0gx517gf.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1cl8lwpikftj0gx517gf.jpg" alt="Maps You Control: A Pure-Vector MapView And Pluggable Native Providers" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Yesterday's &lt;a href="https://www.codenameone.com/blog/funding-open-source-without-the-bait-and-switch/" rel="noopener noreferrer"&gt;funding post&lt;/a&gt; covered the model behind this week's releases. Here is the first one. &lt;a href="https://github.com/codenameone/CodenameOne/pull/5264" rel="noopener noreferrer"&gt;PR #5264&lt;/a&gt; brings mapping back into core with two components: a &lt;code&gt;MapView&lt;/code&gt; that draws an entire map through &lt;code&gt;Graphics&lt;/code&gt; with no native peer, and a &lt;code&gt;NativeMap&lt;/code&gt; that embeds the platform's own map. Both sit behind one &lt;code&gt;MapSurface&lt;/code&gt; API, so you can switch between them by changing a class name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is Codename One?&lt;/strong&gt; Codename One is an open-source framework for building native iOS, Android, desktop, and web apps from a single Java or Kotlin codebase. Learn more at &lt;a href="https://www.codenameone.com/" rel="noopener noreferrer"&gt;codenameone.com&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was wrong with the old maps
&lt;/h2&gt;

&lt;p&gt;For years the answer to "how do I show a map" was one of two things. The deprecated tile-based &lt;code&gt;MapComponent&lt;/code&gt; fetched raster tiles and painted them, which worked but was frozen in time. Or you added the external &lt;code&gt;codenameone-google-maps&lt;/code&gt; cn1lib, which embedded the actual Google Maps SDK as a native peer.&lt;/p&gt;

&lt;p&gt;The cn1lib approach worked, but it locked you to one vendor. It embedded the Google Maps SDK specifically, so a device without Google Play, such as a Huawei phone, was a porting problem rather than a build setting. Being a native peer, the map was also a separate platform surface that only existed where the provider shipped, so it looked different on every platform, because it &lt;em&gt;was&lt;/em&gt; a different map on every platform, and it did not exist at all in the simulator or on the web. None of that makes native maps wrong; it makes them one tool among two, which is exactly what this PR gives you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The MapView vector engine
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;MapView&lt;/code&gt; is the part I find most interesting. It is a pure-vector map. Nothing about it is a native peer. Every road, water body, and park boundary is drawn through &lt;code&gt;Graphics&lt;/code&gt; using &lt;code&gt;GeneralPath&lt;/code&gt; and &lt;code&gt;Stroke&lt;/code&gt;, the same drawing pipeline that paints every other CN1 component.&lt;/p&gt;

&lt;p&gt;The data comes from Mapbox Vector Tiles (MVT). I wrote a new MVT engine on top of the framework's existing &lt;code&gt;ProtoReader&lt;/code&gt; and &lt;code&gt;GZIPInputStream&lt;/code&gt;, so decoding a tile is decompressing gzip, reading protobuf, and turning the geometry commands into paths. There is no third-party parser involved. Because the output is just drawing calls, the map composes cleanly with the rest of your UI. You can put a &lt;code&gt;Button&lt;/code&gt; over it, scroll it inside a &lt;code&gt;Container&lt;/code&gt;, or animate it, and there are no snapshot tricks, because there is no separate platform surface behind the map at all.&lt;/p&gt;

&lt;p&gt;Tile sources are pluggable. The default is raster OSM tiles, but you can switch to the MVT vector source, a bundled source you ship inside the app, or the deterministic &lt;code&gt;DemoTileSource&lt;/code&gt; used by the screenshot tests. Styling is a MapLibre-subset JSON, and two styles ship in the box: light and dark.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Form&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Form&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Map"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BorderLayout&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

&lt;span class="c1"&gt;// A vector tile source (vector=true, zoom 0..14) and the built-in dark style.&lt;/span&gt;
&lt;span class="nc"&gt;TileSource&lt;/span&gt; &lt;span class="n"&gt;tiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;HttpTileSource&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"https://example.com/tiles/{z}/{x}/{y}.pbf"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;MapView&lt;/span&gt; &lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MapView&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tiles&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;MapStyle&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dark&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setCameraPosition&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CameraPosition&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LatLng&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;37.7749&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;122.4194&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addMarker&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MarkerOptions&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LatLng&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;37.7749&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;122.4194&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"San Francisco"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

&lt;span class="n"&gt;hi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BorderLayout&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;CENTER&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;hi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;show&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default constructor uses a raster OSM source and the light style; pass a &lt;code&gt;TileSource&lt;/code&gt; and &lt;code&gt;MapStyle&lt;/code&gt; when you want something else, or switch later with &lt;code&gt;setTileSource(...)&lt;/code&gt; and &lt;code&gt;setStyle(...)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F219m8a98b7297ouviap1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F219m8a98b7297ouviap1.png" alt="A pure-vector OpenStreetMap render of San Francisco in the light style" width="800" height="1734"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;San Francisco rendered entirely through &lt;code&gt;Graphics&lt;/code&gt; in the light style. No native peer is involved; these are paths and strokes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fybcsy5krglh7he25ufmo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fybcsy5krglh7he25ufmo.png" alt="The same area of San Francisco in the built-in dark style" width="800" height="1734"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The same tiles with &lt;code&gt;MapStyle.dark()&lt;/code&gt; applied. The style is a MapLibre-subset JSON, so switching themes is data, not new code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwc1v7qzjhm71ztpx3krx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwc1v7qzjhm71ztpx3krx.png" alt="The vector map with markers placed on it" width="800" height="1734"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Marker&lt;/code&gt;, &lt;code&gt;Polyline&lt;/code&gt;, &lt;code&gt;Polygon&lt;/code&gt;, and &lt;code&gt;Circle&lt;/code&gt; are drawn in the same pass as the map, so a marker is just another shape in the scene rather than an overlay glued on top.&lt;/p&gt;

&lt;p&gt;Because the whole thing is &lt;code&gt;Graphics&lt;/code&gt;, it renders identically everywhere: simulator, device, and the web target included. You host your own tiles and metadata offline if you want, and you control every pixel that gets drawn.&lt;/p&gt;

&lt;h2&gt;
  
  
  NativeMap and the build-hint provider model
&lt;/h2&gt;

&lt;p&gt;Sometimes you want the platform's map, with its road data, its traffic, and its look. That is what &lt;code&gt;NativeMap&lt;/code&gt; is for. It embeds the actual platform map, Apple MapKit or Google Maps, behind the same &lt;code&gt;MapSurface&lt;/code&gt; API. When no provider is wired in, or none is available at runtime, &lt;code&gt;NativeMap&lt;/code&gt; falls back to an embedded &lt;code&gt;MapView&lt;/code&gt; so the code still works.&lt;/p&gt;

&lt;p&gt;The interesting design decision is that the public API never names a provider. There is no &lt;code&gt;GoogleMapsImpl&lt;/code&gt; to import, no &lt;code&gt;NativeInterface&lt;/code&gt;, and no &lt;code&gt;CodenameOneImplementation&lt;/code&gt; hook. You select a provider with the &lt;code&gt;maps.provider&lt;/code&gt; build hint, set to &lt;code&gt;apple&lt;/code&gt;, &lt;code&gt;google&lt;/code&gt;, or another supported value. At build time a &lt;code&gt;MapsProviderInjector&lt;/code&gt; in the builders drops that provider's native implementation into your app's &lt;code&gt;com.codename1.maps&lt;/code&gt; package. Core and the ports carry no map SDK at all, so a provider you do not use costs zero project size.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRECiAgQVsiWW91ciBjb2RlOiBuZXcgTmF0aXZlTWFwKCkiXSAtLT4gQnsibWFwcy5wcm92aWRlciBidWlsZCBoaW50In0KICBCIC0tPnxhcHBsZXwgQ1siTWFwc1Byb3ZpZGVySW5qZWN0b3IgYWRkczxici8-QXBwbGUgTWFwS2l0IGltcGwgaW50bzxici8-Y29tLmNvZGVuYW1lMS5tYXBzIl0KICBCIC0tPnxnb29nbGV8IERbIk1hcHNQcm92aWRlckluamVjdG9yIGFkZHM8YnIvPkdvb2dsZSBNYXBzIGltcGwiXQogIEIgLS0-fG5vbmUgLyB1bmF2YWlsYWJsZSBhdCBydW50aW1lfCBFWyJGYWxscyBiYWNrIHRvIGVtYmVkZGVkIE1hcFZpZXc8YnIvPihwdXJlLXZlY3RvciBHcmFwaGljcykiXQogIEMgLS0-IEZbIlJlZ2lzdGVyZWQgdmlhIE1hcFByb3ZpZGVyUmVnaXN0cnkiXQogIEQgLS0-IEYKICBGIC0tPiBHWyJOYXRpdmVNYXAgcmVuZGVycyB0aGUgcGxhdGZvcm0gbWFwIl0%3D%3Ftype%3Dpng%26bgColor%3Dffffff" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRECiAgQVsiWW91ciBjb2RlOiBuZXcgTmF0aXZlTWFwKCkiXSAtLT4gQnsibWFwcy5wcm92aWRlciBidWlsZCBoaW50In0KICBCIC0tPnxhcHBsZXwgQ1siTWFwc1Byb3ZpZGVySW5qZWN0b3IgYWRkczxici8-QXBwbGUgTWFwS2l0IGltcGwgaW50bzxici8-Y29tLmNvZGVuYW1lMS5tYXBzIl0KICBCIC0tPnxnb29nbGV8IERbIk1hcHNQcm92aWRlckluamVjdG9yIGFkZHM8YnIvPkdvb2dsZSBNYXBzIGltcGwiXQogIEIgLS0-fG5vbmUgLyB1bmF2YWlsYWJsZSBhdCBydW50aW1lfCBFWyJGYWxscyBiYWNrIHRvIGVtYmVkZGVkIE1hcFZpZXc8YnIvPihwdXJlLXZlY3RvciBHcmFwaGljcykiXQogIEMgLS0-IEZbIlJlZ2lzdGVyZWQgdmlhIE1hcFByb3ZpZGVyUmVnaXN0cnkiXQogIEQgLS0-IEYKICBGIC0tPiBHWyJOYXRpdmVNYXAgcmVuZGVycyB0aGUgcGxhdGZvcm0gbWFwIl0%3D%3Ftype%3Dpng%26bgColor%3Dffffff" alt="Diagram" width="877" height="824"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This moves a hard problem from a fork to a build hint. Supporting a device without Google Play stops being "maintain a separate branch" and becomes "set &lt;code&gt;maps.provider&lt;/code&gt; to something else." The provider SPI lives in &lt;code&gt;com.codename1.maps.spi.MapProvider&lt;/code&gt;, and registrations go through &lt;code&gt;MapProviderRegistry&lt;/code&gt;, both covered by the unit tests.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;maps.provider=apple&lt;/code&gt;, &lt;code&gt;NativeMap&lt;/code&gt; renders a live MapKit map with a marker. The screenshot below is from the native Mac desktop build, which supports the Apple provider:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foyub5g59hhqukqra3k6v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foyub5g59hhqukqra3k6v.png" alt="A NativeMap rendering through Apple MapKit on the native Mac desktop build" width="800" height="535"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;NativeMap&lt;/code&gt; with &lt;code&gt;maps.provider=apple&lt;/code&gt; on the native Mac desktop build, showing a real MapKit map and a marker. This is the platform's map embedded, not the vector engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tradeoff
&lt;/h2&gt;

&lt;p&gt;The vector engine is new, and the style format is a subset of a full MapLibre style, not the whole specification. If you bring a complex upstream style it may reference layer or expression features the subset does not implement yet. Labeling and coverage also depend on the tile data you point it at. The default OSM source is fine for general use, but if your tiles lack a layer, the map will not draw what is not there. &lt;code&gt;NativeMap&lt;/code&gt; sidesteps both concerns by deferring to the platform, at the cost of the native-peer behavior the vector path was built to avoid. Pick the one that matches what you are building.&lt;/p&gt;

&lt;p&gt;For confidence there are 29 unit tests in &lt;code&gt;core-unittests&lt;/code&gt;, all green, covering the value types, the model, the provider SPI registry, the MVT decoder across all its value types, the styles, color and zoom handling, cache internals, and the Web Mercator math. The hellocodenameone screenshot tests run against the deterministic &lt;code&gt;DemoTileSource&lt;/code&gt; so the pixels are reproducible offline, and there is a new &lt;code&gt;Maps.asciidoc&lt;/code&gt; chapter in the developer guide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Two components, one &lt;code&gt;MapSurface&lt;/code&gt; API. Reach for &lt;code&gt;MapView&lt;/code&gt; when you want a map that draws like the rest of your UI, renders the same on every target including the web, and lets you carry your own tiles. Reach for &lt;code&gt;NativeMap&lt;/code&gt; when you want the platform's own map and are happy to accept a native peer to get it. The provider for that peer is a build hint, so the choice of Apple versus Google versus none never leaks into your source.&lt;/p&gt;

</description>
      <category>java</category>
      <category>mobile</category>
      <category>android</category>
      <category>ios</category>
    </item>
  </channel>
</rss>
