Run Flutter widget code on Codename One - #5883
shai-almog wants to merge 224 commits into
Conversation
Dart→Java 17 transpiler (maven/dart-transpiler, ANTLR front end extended for Dart 3 syntax), dart:core/async runtime (maven/dart-runtime), and the Flutter widget framework on CN1 components (maven/flutter-runtime): element reconciliation, box-constraint layout, ~45 widgets, theming/dark-mode, async/await, Navigator, input widgets. transcode-flutter mojo + archetype wiring; hermetic project-local .m2 with repo forwarding in run/debug/sim mojos. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ing-up Transpiler (dart-transpiler): the full 159-file Flutter Gallery now transpiles to 0 errors and the 563 emitted Java files compile clean, then boot and run in the JavaSE simulator. Key correctness fixes, all general (not gallery-specific): - Static-init ordering: emit static fields in dependency order (direct refs + same-class static-method-call transitive deps) so Java's top-to-bottom static init matches Dart's lazy/order-independent semantics. - Null-shorting: a Dart `a?.b.c()` now guards the whole trailing selector chain via a propagated short-guard on Out, materialized at value consumption. - Import-scoped class resolution: a simple name shared across files resolves via the referencing library's imports (Program.resolveClass + emitCtorCall), not a flat last-registered map. - Captured for-loop var gets a per-iteration effectively-final alias. - Nested-switch trailing-break no longer double-emits an unreachable break. Runtime (flutter-runtime, dart-runtime): MaterialApp resolves onGenerateRoute for the initial route (routing-based apps with no home:); Localizations pipeline wired (delegate.load -> LocalizationsScope InheritedValueProvider -> Localizations.of); Future.getNow() for synchronous results; plus the widget/type surface added across the transpiler passes to reach 0 compile errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PositionedTransition rendered its child through a passthrough element, so a Stack never positioned it (the gallery Backdrop's sliding home/settings panels stayed unplaced). Resolve the animation's current RelativeRect and host the child as a Positioned (LTRB insets) so the Stack lays it out; at rest the home fills and settings sits off the top edge. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Four fixes take the transpiled Flutter Gallery from an empty home container to a recognizable home (Gallery title, study carousel, Material/Cupertino category lists): - ValueListenableBuilder.build now invokes its builder(context, value, child) with the listenable's current value instead of returning the (usually null) pass-through child. The home's whole subtree is produced by such a builder, so it previously never built. (High-leverage: used across the app.) - AdaptiveBreakpoints.getWindowType returns the real bucket from the window's LOGICAL width (device px / Dp.scale) instead of a hardcoded `medium`, so a phone-sized window gets the mobile layout (isDisplayDesktop was always true). - StatefulElement.firstBuild now runs didChangeDependencies after initState and before the first build, matching Flutter; widgets that create controllers there (a PageController sized from MediaQuery) no longer read null. - Transpiler: recover a dropped inferred <T> witness for BuildContext ancestor lookups (X.of(context) => context.dependOnInheritedWidgetOfExactType()) from the enclosing return type, so SplashPageAnimation.of etc. resolve. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Image.asset() never resolved: the runtime asked for "/assets/<path>", but CN1's getResourceAsStream rejects any nested resource name on every port (JavaSEPort.java:15733 -- "resources cannot be nested in directories"). The asset tree is now flattened at build time and re-derived at runtime through the same encoding: every '_' doubles, '/' becomes '_', under a "cn1f_" prefix. That is unambiguous, introduces no character that was not already legal in the asset path, and preserves file extensions so native bundlers still classify a .png as a .png. - FlutterAssets: the encoder + the contract it upholds, with tests covering separator/escape collisions, package asset paths and the reserved "raw" prefix. - ImageRenderElement resolves through it (and names the resource it missed). - TranscodeFlutterMojo writes src/main/flutter/assets flattened rather than mirroring the tree. FadeInImage also rendered nothing at all -- build() returned an empty SizedBox, so study cards showed only their background colour. It now builds an Image from its provider (placeholder as fallback); the cross-fade stays deferred. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Flutter's Scaffold is a Material -- opaque, not a transparent frame. Ours created no component at all, so in the gallery's backdrop (settings page and home page stacked) the settings page showed through every gap between the home page's children. ScaffoldRenderElement now creates a "FlutterScaffold" face filled with Scaffold.backgroundColor, else theme.scaffoldBackgroundColor, else colorScheme.background -- Flutter's own resolution order -- and repaints it on theme change. Children attach after it in tree order, so they still paint on top. Also refreshes ThemeDataAdapter's javadoc and ThemingTest for the Material 3 app-bar default: the adapter moved from colorScheme.inversePrimary to surface (matching AppBarRenderElement) but the doc and the assertion were left behind, so the suite had a standing failure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Flutter picks the asset variant authored for the screen's density -- dir/3.0x/name.png on a 3x screen -- and the gallery ships 1.5x through 4.0x. We were always loading the unscaled file and upscaling it, so every bundled image was soft. FlutterAssets.open now probes candidates in Flutter's preference order (the smallest variant at least as dense as the screen, then denser, then the closest lower ones, then the unscaled asset) and reports which density it found. Flutter reads that set from a build-generated manifest; probing gets the same answer without one, and a missing variant just falls through. ImageRenderElement rescales the natural size by screen-density/asset-density, so an unsized image occupies the same logical box whichever variant backs it. Adds a CategoryHeaderShapeTest that pins the gallery category header's geometry layer by layer (Wrap, Row/Expanded, SizedBox, Material, Container), so a card that looks wrong on screen can be attributed to a node instead of eyeballed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…gates
Only the home screen was reachable. NavigatorState was a stub whose every
method returned without doing anything, so the gallery's
Navigator.of(context).restorablePushNamed('/demo/<slug>') -- how it reaches
all ~130 demos and all 6 studies -- silently did nothing.
- Navigator now resolves a route NAME the way Flutter does: the `routes` map,
then onGenerateRoute, then onUnknownRoute. MaterialApp publishes its table
on build (and gains the missing onUnknownRoute), and its own initial-route
lookup goes through the same resolver rather than a private path.
- NavigatorState's named/replacement/push/pop/popUntil surface is wired to
that resolver instead of returning null. An unresolvable name is logged,
not thrown -- a dead link in one corner of an app should not take it down.
- Navigator.of(context) now returns a handle BOUND to the calling context,
because a push has to know where it came from:
A pushed route mounts as a fresh element-tree root (its own CN1 Form), so
its ancestor chain ended immediately and every Foo.of(context) inside the
page resolved to null -- the gallery's pages died on
GalleryLocalizations.of and GalleryOptions.of. In Flutter a route builds
below the app and inherits everything above it. Element now continues an
exhausted lookup from a `contextFallback` -- the context that pushed the
route -- which restores the inheritance WITHOUT joining the two trees
structurally, so the render/host logic still sees a genuine root (a root
Scaffold must keep owning its Form's Toolbar).
Also publishes tap targets to the accessibility tree: a GestureDetector or
InkWell with an onTap now exposes the button role and an activate action on
its overlay -- the only component that knows the subtree is tappable. Without
it a tappable Flutter subtree was invisible to screen readers and to anything
driving the UI through semantics.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Colors.transparent is 0x00000000 — alpha 0 over BLACK — and the gallery uses it for app bars and scaffolds meant to show what is behind them. paintSolid took only the RGB word and forced full opacity, so every one of those became an opaque black band across the page. Adds ThemeDataAdapter.paintColor, which carries the alpha through and paints nothing at all when it is zero; Scaffold and AppBar backgrounds now go through it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PageView reused the vertical scroll boundary and stacked its pages in a Column, so the gallery's home carousel ran DOWN the page: only the Reply card was on screen and Shrine/Rally/Crane/Fortnightly/Starter sat far below the fold. - ScrollRenderElement gains an axis. A horizontal boundary lays its content out with a tight viewport height and an unbounded width — the vertical contract mirrored — and hands CN1 an X-scrollable pane driven by the new HorizontalScrollRootLayout. - PageView lays its pages along that axis, each sized to the controller's viewportFraction of the viewport. That fraction is the whole point of the widget's look: a value below 1 is what makes the neighbouring pages peek in at the edges, so it has to reach the pages as a real constraint. It also paints no scroll indicator, matching Flutter, where the peeking pages ARE the affordance. - ListView(scrollDirection: Axis.horizontal) rides the same axis support. Its windowing stays vertical-only — the scroll math and spacers are written against item heights — so horizontal lists build eagerly, which is what a row of cards wants anyway. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CustomPaint rendered its child (or an empty box) and never called the painter: Canvas was an API-shaped stub whose every method did nothing. The gallery's settings gear, the Rally charts and every other hand-drawn widget were therefore blank boxes. GraphicsCanvas implements the dart:ui Canvas against CN1's Graphics: - Transforms are kept HERE as a 2x3 affine matrix and every coordinate is mapped through it before reaching Graphics, rather than leaning on CN1's optional Transform support — a painter must not silently draw untransformed geometry because a port lacks a capability. - Painters work in LOGICAL pixels, so the canvas starts pre-scaled by the device pixel ratio and the painter is handed a logical-pixel size. A painter written against Flutter's coordinate system lands at the right physical size on any density. - Rects, rounded rects, ovals, arcs, lines and full paths become GeneralPaths in device space; Paint's style/width/cap/join/alpha drive fill vs stroke. A misbehaving painter is logged, not allowed to take the frame down. Two real ParparVM API gaps surfaced while compiling the iOS build of this app, both CLDC-era omissions that would hit any app doing arithmetic: - java.lang.Math was missing acos/asin/atan2/exp/log/log10 — added with C natives and the matching JS-port bindings, so the methods exist on every backend rather than only where they were needed today. - java.lang.Long was missing the Java 8 static hashCode(long); the instance method now delegates to it. Also fixes a latent ordering bug the horizontal carousel exposed: a PageView page sizes itself against the VIEWPORT, but read it from size(), which is only assigned after layout returns — so pages measured against a stale (initially zero) extent and the carousel came up blank. The scroll boundary now reports its viewport before laying the content out. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…artLongList intrinsics The transpiled gallery launched on the iOS simulator and died immediately on "Pattern.compile() not implemented on this platform": dart:core's RegExp was backed by java.util.regex, which ParparVM does not implement. The gallery matches every route name with a RegExp, so nothing rendered at all. RegExp now runs on Codename One's own engine (com.codename1.util.regex.RE) — plain Java that translates like any app class, so the behaviour is the same on every target instead of only where java.util.regex happens to exist. Its Perl5 syntax covers what Dart's ECMAScript grammar uses in practice (anchors, classes, quantifiers, alternation, groups); unicode/dotAll have no engine counterpart and are recorded but inert rather than silently changing a match. RegExpMatch now SNAPSHOTS its groups and offsets: the engine carries match state on the compiled pattern and overwrites it on the next match, so a Match reading through to it would change under the caller — a Dart Match is a value. Two more gaps found by compiling the iOS build: - java.util.Collections was missing emptyIterator(). - The translator renames devirtualized DartLongList element access to cn1InlDllGet/Set, but those intrinsics were never added to cn1_intrinsics.h — so every app hitting that path failed to compile for iOS. Added, bounds-checked against the logical length exactly as DartLongList is, falling back out-of-line so RangeError stays single-sourced. Also: a pushed route whose Scaffold is nested (the gallery's demo pages sit inside a ColoredBox) draws its own in-canvas AppBar, so the Form's Toolbar is now hidden for it instead of adding a second bar — its inset was shrinking the Flutter canvas and leaving those pages floating inside a margin. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…from Flutter Forms A transpiled `!` failure reported only "Null check operator used on a null value" with a stack of nothing but the framework's own recursion — the transpiled build methods are inlined into their caller's frame on ParparVM, so the trace named no application code at all. The runtime now carries a diagnostic context that the element tree sets around each build, so the error reads "... (while building GalleryApp)" — Flutter reports the error-causing widget for the same reason. In the same spirit: - A failed inherited-widget lookup now names the type it wanted and lists the ancestors it actually walked, instead of returning null and letting the app die on `Foo.of(context)!` somewhere else. - MaterialApp no longer swallows a localizations delegate's failure; it says which delegate failed for which locale. Flutter Forms also had CN1 chrome padding on the Form and its content pane. Flutter owns the whole canvas and draws its own padding and safe areas, so that inset is a margin it never asked for — it was framing pushed pages. Fixes ParparVM's Class.isAssignableFrom, which passed its arguments to instanceofFunction(source, dest) backwards: A.isAssignableFrom(B) asks whether B is assignable to A, so the ARGUMENT is the source and the receiver the destination. It answered the reverse question. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MaterialApp loaded its localizations during its own build and, when that produced nothing, installed no Localizations scope at all — so every `Foo.of(context)!` below died with no indication why. Two changes: - The scope is now installed unconditionally and resolves its resources on first lookup. Loading during build reads the app's delegate list at the earliest possible moment, which on a lazily-initialised backend can precede the static initialiser of the class holding it. - Localizations.of no longer swallows a throwing lookup, and says when a lookup simply found nothing. Diagnostics only where the runtime previously went quiet; no behaviour change on a healthy app. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… locale The whole inherited-widget mechanism rested on Class.isInstance alone, so a backend answering it incorrectly would take every Foo.of(context) down with it. Element.isInstanceOf now also walks the object's own superclass chain, which needs no reflection beyond getClass()/getSuperclass(). MaterialApp also logs the locale it resolves localizations for — a delegate with no table for that locale contributes nothing, and the app then dies on Foo.of(context)! elsewhere. (These did not fix the iOS-only localizations failure being chased; the resource list is non-empty but carries no GalleryLocalizations, so the gallery's own delegate is producing null there. Kept because both are correct on their own terms.) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…uced Chasing an iOS-only localizations failure, two diagnostics went missing themselves: the reports build their message by walking ancestors and calling getClass() on each, and a throw anywhere in that walk took the whole report down with it. The summary line is now emitted on its own, before the walk. MaterialApp also logs what each localizations delegate returned. The resource list is what every Foo.of(context) searches, so when a lookup finds nothing the contents of that list are the first thing worth seeing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
instanceofFunction's parameter names say (sourceClass, destId), but the
convention its only real caller establishes is the opposite: BC_INSTANCEOF
passes the bytecode's TYPE operand first and GET_CLASS_ID(obj) second, and
the body indexes classInstanceOf[] by the OBJECT's class — whose generated
table lists that class's supertypes — then searches it for the target.
Class.isInstance passed those the other way round, so it searched the
TARGET's supertype table for the object's class and answered false whenever
the object was a strict subclass. Verified in a generated build:
classInstanceOfArr694 (GalleryLocalizationsEn) = {1428, 303, -1}
classInstanceOfArr1428 (GalleryLocalizations) = {303, -1}
so isInstance(GalleryLocalizations, aGalleryLocalizationsEn) looked for 694 in
the second table and said no. Every Class.isInstance in a native build was
wrong unless the two types were exactly equal — which is why the transpiled
gallery's Localizations.of(context) resolved to null on iOS while working on
the JVM.
Both isInstance and isAssignableFrom now pass the receiver Class first, and a
comment above them records the argument order so the misleading parameter
names cannot mislead again. (An earlier commit in this branch "fixed"
isAssignableFrom on the strength of those names; it was correct and is
restored here.)
Also adds the missing Class.getSuperclass(), and drops the speculative
superclass-walk fallback from Element.isInstanceOf — it was papering over
this bug, and it called getSuperclass before ParparVM had it, which broke the
iOS build.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ying or going quiet
Two failure modes were invisible during development.
An uncaught error on the EDT raises Codename One's modal error dialog, which
blocks the EDT — so the first broken screen stalled every screen after it, and
"this screen is broken" became "the app is dead". FlutterErrorReport.install()
collects instead: it consumes the error, records it with the widget that was
building and the route that was on screen, counts repeats rather than
reprinting them, and lets the app carry on. That context is what makes a report
usable, because transpiled build methods are inlined into the framework's frame
on some backends and the stack alone names nothing but Element.updateChild.
The quieter mode is worse: a screen comes up blank and NOTHING throws, because
the widgets it needs are stubs that pass their child through or draw nothing. A
sweep of the gallery reported zero errors against six blank screens. Eighteen
such widgets now declare their gap through the same channel, so a sweep answers
"what did this screen need and not get" instead of shrugging:
18x OpenContainer: the container transform renders nothing [route /demo/motion]
6x Transform: scale, rotation and translation are ignored
5x Opacity: opacity is ignored; the child paints fully opaque
Inherited-scope widgets that legitimately pass their child through (Theme,
MediaQuery, IconTheme, FocusScope...) are deliberately not reported.
Opt-in, not automatic: a shipping app wants the dialog or its own crash
reporting, so runApp does not install this.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both passed their child straight through, so every Transform.scale and Opacity in an app was computed and discarded. The gallery's carousel drove a scale animation at 60fps that could not possibly show. The render tree is flat — every element's component is a sibling in one host container, positioned absolutely — so an ancestor cannot wrap its descendants in a paint effect, because they are not its children. EffectRenderElement gives such a widget a nested container with its own RenderHost, the same device the scrollables already use, which makes the subtree genuinely nested and therefore paintable through. Layout is untouched: the child measures against the incoming constraints and the effect takes exactly its size, as in Flutter. - Opacity composites the whole pane through the Graphics alpha, so overlapping children fade as one layer rather than tinting individually, and nested Opacity multiplies. - Transform scales/rotates about the element's centre (Flutter's default alignment, and what the carousel expects), and translates by shifting the origin — which needs no matrix support and so works on every port. A port without transform support still gets the translation and reports the rest instead of dropping it silently. An explicit origin/alignment is not honoured yet and reports itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every AnimationController chained its own setTimeout(16), so N concurrent animations meant N timers and N wakeups — and because each tick marks its listeners dirty and the build owner then revalidates the affected host, N rebuild/relayout passes per frame instead of one. The gallery's home screen runs several at once (an entrance animation per category item, a scale per carousel card), which is a large part of why it felt heavy. Controllers now register with a shared FrameDriver and are advanced together: one wakeup, one batch of notifications, one build flush per frame. The clock stops itself when the last animation finishes, so an idle app runs no timer at all, and a controller that throws is removed rather than stopping the clock for everything else. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rounded corners and a drop shadow are what make a Material surface read as Material, and both were being dropped: - MaterialRenderElement set a flat bgColor and ignored `shape` and `elevation` entirely, so the gallery's category rows were flat rectangles. - FlutterBoxStyle handled circles and flat colours but ignored a BoxDecoration's borderRadius and boxShadow. Both now build a RoundRectBorder from the radius (and a shadow scaled from the elevation), matching what CardRenderElement already did. CN1 draws one radius for all four corners, so a decoration with mixed corners takes its top-left — closer than dropping the rounding altogether. Found by putting our home screen next to the native Flutter gallery's on the same simulator, which is the comparison that should have been driving this all along: the structural audit counts nodes and is blind to shape, elevation, insets and typography — the things that actually make it look wrong. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eports its gap Follow-up to the shape/elevation work. The gallery's study card is a Material with elevation 4, a 10dp radius and Clip.antiAlias, and it still looked square: the cover image fills the surface and, in a flat render tree, paints as an unrelated sibling over the rounded background. So Material now nests its subtree through EffectRenderElement, which is the prerequisite for clipping it. The clip itself is NOT done. An attempt to cut the subtree with a rounded-rect path built from absolute coordinates cut away most of the content — the category labels vanished and the card image was sliced — because setClip(Shape) does not share the coordinate space the rest of the paint path uses. Reverted rather than shipped: a square corner is a blemish, a missing label is a broken screen. The gap now reports itself instead of being a silent visual difference. Style derivation is also cached against a signature, so a RoundRectBorder is not rebuilt on every frame of every surface. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A viewportFraction below 1 does not just make the pages narrower — it centres the current one, resting the scroll at -(1-f)*viewport/2 so the page sits inset with its neighbour peeking. Ours started at offset zero, so the first card was flush against the leading edge and all the slack piled up on the trailing side. Measured against the native gallery on the same simulator: the study card's left margin was 2.1% of the screen where Flutter puts it at 13.2%. The slack is measured at LAYOUT time, through the same viewport hook the pages use. Building it during buildContent would always compute zero, because that runs before the viewport is known — the same trap that made the pages themselves collapse earlier. Left margin is now 11.6% against Flutter's 13.2%. The remaining difference is card width (78.9% vs 83%), which is a separate question about how the card's own width and padding resolve inside the page. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… type The erasing cast exists for a subtype whose type ARGUMENTS differ from the target - MaterialPageRoute<Void> reaching a Route<Object>. Its guard asked only whether the value was a subtype at all, so it also fired on the ordinary case where the subtype already inherits exactly the instantiation being assigned to. Every StatefulWidget paid for it: createState() returned (State<MyHomePage>) (Object) (new _MyHomePageState()) where plain new _MyHomePageState() is what Java wants, across all 537 generated files. Resolve what instantiation the value inherits and skip the cast when it already matches, walking the same superclass chain isSubtypeName walks and substituting each class's type parameters on the way down. The goldens caught this, and reseeding them also picks up the library-privacy change they had been left behind by: a Dart `_name` member is private to the LIBRARY, not the class, so sibling classes reach it and its accessors are emitted package-private rather than skipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…here they draw Five defects on the gallery home screen, all of them things the widget was already asked to do and silently did not. Align ignored widthFactor/heightFactor. It stored both and read neither, so it always filled the bounded axes. That is the geometry an expand/collapse runs on: the gallery's category and settings lists animate ClipRect(child: Align(heightFactor: t, child: ...)) with t from 0 to 1, and with the factor dropped every frame of that animation laid out identically. Now the box takes the Flutter fraction of the child while the child keeps its full size. ClipRect did not clip - it passed its child straight through, so there was nothing to hide the part of the child that does not fit yet. It gets the nested pane EffectRenderElement already provides for Transform and Opacity: the render tree is flat, every element absolutely positioned as a sibling in one host, so a widget can only affect its descendants' paint by nesting them. The clip itself is free once nested - Component.internalPaintImpl already confines a component's paint to its bounds. CustomPaint anchored its canvas at getAbsoluteX()/getAbsoluteY(). A Graphics being painted through has already accumulated its ancestors' translation (Container.paintChildren translates on the way down), which is why the whole of Codename One draws with getX(). The absolute origin added that offset a second time and pushed the drawing outside the bounds the component clips to - the painter ran every frame and produced nothing. The gallery's settings icon was the blank white notch in the top right. arcTo dropped its forceMoveTo flag and always started a new subpath. Flutter joins the arc to the current point with a line when forceMoveTo is false, which is how two opposing half-circle arcs become one stadium; ours produced two disconnected discs, so each stick of the settings icon painted as a pair of dots. A Paint carrying a shader has no colour of its own and the canvas only ever read paint.color(), so anything drawn with a gradient came out black - including both sticks of that icon, whose whole identity is being pink and teal. Fill the shape with the gradient by clipping to it and running Codename One's linear gradient over its bounding box; a port without shape clipping fills solid with the ramp's midpoint, since the shape matters more than the ramp. Verified in the simulator against the native Flutter capture: the settings icon now draws a pink stick and a teal stick with their knobs where it drew a blank notch, and tapping a category reveals its demo rows. The animation's intermediate frames are not verified - a screenshot round-trip through the simulator's MCP server costs ~0.6s against a ~200ms animation, so the geometry is pinned by AlignFactorTest instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ment Measured on the gallery home: tapping one category cost a 615ms frame, of which 592ms was layout. That is 37x the 16fps budget, and it is what "unresponsive" actually was - input dispatch itself was already free (0ms per drag event), and an idle app runs no frames at all. The cause: every box cached ONE (constraints -> size) result, and Codename One asks a container two different questions. getPreferredSize measures with loose unbounded constraints; layoutContainer lays out with tight ones. The two alternate, so each evicted the other on every box in the tree and a single changed leaf re-measured everything: 7635 layout calls at an 8% hit rate, 6287 of the misses purely because the constraints differed rather than anything being dirty. Give the dry measurement its own cache slot, as Flutter does with _cachedDryLayoutSizes, and let dryness propagate: performLayout measures its children through layout(), so without a flag the dry pass writes dry constraints into every descendant's real slot and the following real pass misses on all of them - only the root would have benefited. The subtlety that makes this correct: performLayout is NOT side-effect free, it writes child offsets. A dry pass that actually runs leaves those offsets at dry values, so the real pass must not be allowed to hit its cache and keep them. It drops its own real slot only - not its ancestors', which would escalate back into the whole-tree invalidation this exists to avoid. Caught in the simulator: without it the study card's caption rendered at the top of the card instead of the bottom. BuildOwner.traceFrames() records what a build flush costs and how much of the layout pass the cache absorbs, since "the UI feels slow" is a question about where the frame went. Result: worst frame 615ms -> 229ms, layout calls 7635 -> 4179, hit rate 8% -> 30%. Better, NOT fixed - 229ms is still ~14x the frame budget, and the remaining cost has moved out of layout-call count into the revalidate itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A build flush called RenderHost.revalidate(), which called revalidateWithAnimationSafety() - the heaviest option available. In Codename One a finished layout is finished; revalidate goes to the Form root and lays the whole hierarchy out again, so a setState on one leaf re-laid out the toolbar, the side menu and every other container on the form. Mark this host's own subtree and call layoutContainer(), which does only the work a change inside this host can have affected. The Flutter pass is tight against the host's bounds, so the host does not change size and its parent has nothing to redo. This is the right scope, but it is NOT where the time was going: the frame stayed at ~200ms, because the cost is our own constraint pass over the host subtree rather than Codename One laying out the rest of the form. Recording that here so the next attempt does not re-try this avenue expecting a win. Adds per-class self-time attribution to the layout pass (parents would otherwise swallow their whole subtree and every profile would blame the root), reported through frameStats as the hottest element classes. The remaining ~200ms is roughly 4000 layout calls, far too slow for constraint arithmetic, so the next question is which element's performLayout is expensive - and now the runtime can answer it instead of being guessed at. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s every frame 615ms -> 26ms for the frame a category expand costs; the layout part of it, 592ms -> 1ms. The Flutter constraint pass was never the expensive thing. Per-class self-time attribution puts it at ~2ms across ~4900 boxes - TextRenderElement 2ms and every other element class rounding to 0. All the rest was Codename One being told to redo work it had already done and cached. revalidate() begins with setShouldCalcPreferredSize(true), and that recurses down every child container discarding CN1's cached preferred sizes, so every Label re-measured its text on every frame. Preferred size is cached precisely so that does not happen. Switching to layoutContainer() kept the same mistake, because I was still calling setShouldCalcPreferredSize(true) to mark the subtree - which is why that change did not move the number. A Flutter build flush needs neither. Our own layout writes every component's bounds absolutely, so running the host's Layout directly is the entire job: no invalidation, no CN1 measurement, no walk of the form. A component whose content actually changed already invalidates itself - Label.setText does - so blanket invalidation could only ever discard measurements that were still valid. Verified in the simulator: the category expand still reveals its rows and the home screen renders identically to the reference capture. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The study card is Material(shape: RoundedRectangleBorder(10), clipBehavior: Clip.antiAlias) with an image filling it. We drew the rounded surface but never clipped the child, so the image painted square corners over it and the card read as a plain rectangle against the reference's rounded one. Material already owns a nested pane (it is an EffectRenderElement), so the clip just needs the right shape in the right space: the path is built from the pane's PARENT-RELATIVE bounds, because the Graphics has already accumulated its ancestors' translation. The earlier attempt at this built the path from getAbsoluteX/Y, which is the same double-offset that made CustomPaint draw nothing, and is why it "cut away most content" and was reverted. setClip(Shape) replaces the clip rather than intersecting it, and the card sits in a horizontally scrolling carousel that is already clipping us, so replacing outright would let a half-scrolled card paint outside its viewport. Intersecting unconditionally is not the answer either: GeneralPath.intersection() does not survive a path that lies entirely inside the rectangle, and going through it in every case cut the icons out of every category row. Intersect only on genuine overflow; use the plain rounded rect otherwise. Verified against ref-flutter-home.png: the card now has rounded corners and the category rows keep their icons. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four tests reproducing the gallery's carousel card - Container(padding horizontal 4, margin vertical 16, height 240, width 296) inside a 240-tall viewport - and asserting Flutter's answer as measured from the native app: a 288 x 208 surface, both vertical margins coming off the height because the height is a ConstrainedBox inside the margin that the viewport's own maximum clamps. They pass, which is the useful part: the container maths is right, so the card rendering 296 x 224 in the running app is not this code getting the arithmetic wrong but the widget receiving different values than the Dart specifies. That narrows the remaining 16px of height and the missing 8px of padding to the transpiled configuration rather than the layout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
✅ Continuous Quality ReportTest & Coverage
Static Analysis
Generated automatically by the PR CI workflow. |
Cloudflare Preview
|
codenameone-dart-runtime and codenameone-flutter-runtime compile at release 17, and the framework's own build runs on JDK 8, so listing them unconditionally made every JDK 8 leg fail while compiling them -- and the failure named a module that had nothing to do with whatever the change was. build-linux-jdk8, archetype-smoke, protocol-e2e and the three JavaSE CEF smoke tests all died at the same step for this reason. They move behind a profile that activates on JDK 17 and newer. dart-transpiler stays in the default reactor deliberately. It targets 1.8 and codenameone-maven-plugin has a compile dependency on it for the transcode-flutter goal, so removing it would break the plugin on exactly the JDK the plugin is built with. Its test dependency on the runtime is what had to move instead: the tests transpile against the real runtime, which drags both release-17 modules in, so the dependency is now added only on JDK 17 and the tests are skipped below it rather than left to fail compiling. Verified: a JDK 8 reactor resolves dart-transpiler alone and builds it clean; a JDK 17 reactor resolves all three and runs the transpiler's 47 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drawing the shape straight onto the Graphics anti-aliases its outline against whatever is really behind it, while the image path anti-aliases against transparency and then composites, quantising alpha twice. The two agree to within a fraction of a pixel along the edge -- invisible to a person, and still a difference a screenshot test measures: the developer guide's components-toggle-buttons-ios figure moved 0.545% of its pixels against a 0.35% allowance. That is a compatibility change to every application already using this border, for a benefit only a shape whose SIZE ANIMATES actually needs. So it is now requested per border with directPaint(true), the flutter runtime asks for it on the circle it animates, and every other caller keeps the rendering it has. Verified by regenerating all 84 guide figures and running the comparison CI runs: clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The workflow set up JDK 8 and then JDK 17 and built the framework in every platform job. Two things wrong with that, both of which CI found: The last setup-java wins, so the build ran on 17 -- and codenameone-javase imports javafx.*, which no JDK after 8 carries, so it failed compiling the JavaSE port. And macOS arm64 runners have no Temurin 8 at all: setup-java reports "Could not find satisfied version for SemVer 8", the oldest offered being 11. The framework is now built once in the container pr.yml already uses, which bakes JDK 8 and cn1-binaries: a JDK 8 pass for everything targeting it, then a JDK 17 pass for the two runtime modules, into one local repository that is uploaded and consumed by the platform jobs. Those need JDK 17 only, because they build the application rather than the framework -- which is also six framework builds less per run. Also closes the CodeQL finding on tree_size: the except body says why ignoring the error is intentional, which is that a build tree is live while this walks it and a file that has gone is a file the artifact does not ship. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Compared 172 screenshots: 172 matched. Benchmark ResultsDetailed Performance Metrics
|
|
Compared 157 screenshots: 157 matched. Native Android coverage
✅ Native Android screenshot tests passed. Native Android coverage
Benchmark ResultsDetailed Performance Metrics
|
|
Compared 172 screenshots: 172 matched. Benchmark ResultsDetailed Performance Metrics
|
|
Compared 172 screenshots: 172 matched. |
|
Compared 172 screenshots: 172 matched. |
|
Compared 172 screenshots: 172 matched. Benchmark ResultsDetailed Performance Metrics
|
ContainerTransformTransition and CupertinoPageTransition expose only private constructors and nothing extends either, so both are final. The unused Display import goes, and dominantColor's index loop becomes a foreach since the index only ever indexed. The cast in dominantColor is the one that matters. It sits inside a catch(Throwable) that exists to make the method total, and ParparVM's CHECKCAST is unchecked -- a failed cast does not throw there, so the handler would never run on iOS and the wrong object would simply be read as an Integer. Guarded with instanceof, which is the form the verifier recognises and which is what the code meant anyway. Verified: check-cast-semantics.sh reports no new reliance (87 baselined). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The archetype now defaults javaVersion to 17 (Flutter support needs it, and it matches start.codenameone.com), while several CI legs deliberately run the integration suites on JDK 8 so the framework is built the way it has to be. The two met in five scripts that generate a project and then build it, and the result was "invalid target release: 17" -- a failure about the generated project that reads like a broken archetype. inc/env.sh now derives CN1_ARCHETYPE_JAVA_VERSION from the running JDK and every archetype:generate passes it, so each leg tests a pairing that can actually exist: a JDK 8 leg generates and builds an 8 project, a modern leg generates and builds a 17 one. Override the variable to test a specific pairing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The guide allows no inline source blocks: every snippet is included by tag from docs/demos, and Java snippets must live under a compiled source root so an example naming an API that has since changed fails the build rather than misleading a reader. The new chapter had four inline blocks. The XML and shell examples move to src/main/snippets. The two Java examples become FlutterInteropSnippets in the demo module, which means the demos now carry codenameone-flutter-runtime at provided scope and the snippet validator's compile classpath knows about it. Both examples now compile against the real FlutterUI. Also adds packages: read to the benchmark workflow. The framework job runs in the pr-ci-container image on ghcr, and without that permission it dies in "Initialize containers" with a bare "Error response from daemon: denied", which reads like the image is missing rather than like the token cannot see it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The pr-ci-container supplies JAVA_HOME_8 and JAVA_HOME_17, which is how pr.yml selects between them. This job hard-coded /usr/lib/jvm/java-8-openjdk-amd64 instead and died on "The JAVA_HOME environment variable is not defined correctly" -- a message that says neither which JDK was wanted nor what was actually there. It also no longer runs setup-java for 17, which downloaded a second JDK into an image that already has the right one. A check up front fails with the variable name and the path it found when either JDK is missing, so the next person to meet this reads the cause rather than inferring it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The javaVersion argument went in without the trailing backslash, so the mvn
invocation ended at it and the next line ran as a command of its own:
cn1app-archetype-test.sh: line 27: -DinteractiveMode=false: command not found
`bash -n` passes that, because two commands is valid shell -- which is why
checking the syntax was not enough to know the edit was right.
Verified by running the generate: under JDK 8 it resolves javaVersion to 8,
writes java.version=1.8 into the generated project, and that project's common
module builds on JDK 8, which is the combination that was failing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Developer Guide build artifacts are available for download from this workflow run:
Developer Guide quality checks: |
-am pulls codenameone-javase into the reactor, and that module needs -Plocal-dev-javase. Without it the build died in javase's cn1-generate-build-hint-data execution -- a long way from anything this workflow is about, and the third failure in a row on this job that I diagnosed from a CI log rather than a local run. Verified locally this time: the same -pl set with -am does put codenameone-javase in the reactor, and the whole build succeeds on JDK 8 with the profile. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The goal invokes archetype:generate as a SEPARATE Maven process, and that process inherits none of this one's -D properties -- only the fixed list the mojo builds reaches it. So -DjavaVersion was silently dropped and the archetype used its default of 17, which is why tests/core.sh still failed with "invalid target release" on the JDK 8 legs after the integration suites were fixed: it goes through this goal rather than through archetype:generate directly. The derivation itself moves to scripts/ci/archetype-java-version.sh, shared by tests/env.sh and maven/integration-tests/inc/env.sh so the two suites cannot drift apart on it. Verified locally: tests/core.sh passes on JDK 8 and the project it generates carries java.version 1.8. Also clears the guide's Vale findings on the new chapter -- the house style wants contractions and no cliches, and the chapter had 24 -- and gives GenerateAppProjectMojo the copyright header it was missing, which the diff-scoped gate demands now the branch touches the file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The gallery is not a standalone package, and lifting it out of the Flutter SDK's pub workspace lost two things that the workspace root was providing. Its LOCKFILE. The gallery pins almost nothing -- google_fonts is declared as `any` -- so the only thing holding its dependencies at the versions the Flutter team tests is the workspace root's pubspec.lock. Without it pub resolved google_fonts 8.1.0 instead of 6.2.1 and three studies failed to compile on "Member not found: 'GoogleFonts.robotoCondensed'". The benchmark would have been comparing against a gallery that does not build, on a dependency nobody chose. Its ASSET PACKAGES. flutter_gallery_assets, rally_assets and shrine_images are declared once at the workspace root for every member, so a lifted package loses them and the build stops at "Could not resolve package for asset packages/rally_assets/logo.png" -- an error about an asset, for a dependency that is simply absent. They are derived from the pubspec's own asset paths rather than listed here, because a list would be wrong the first time the gallery gained a fourth and would fail in the same indirect way. Also installs GTK3 and a C toolchain on the Linux leg: Flutter's Linux build and Codename One's native Linux port both compile against it, and without it CMake stops at "The following required packages were not found: gtk+-3.0" before either side produces anything. Verified: a prepared tree resolves google_fonts 6.2.1, declares all three asset packages, and `flutter build web --release` completes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It instrumented edtLoopImpl -- the hottest loop in the framework -- with eight conditional timestamp blocks and a per-pass report, plus a matching per-container timer in Form.flushRevalidateQueue. Debugging scaffolding does not belong in a hot path, whatever it costs when switched off. Time to first form on screen is available without any of it: a Form knows when it is shown, so onShowCompleted is the place to ask. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
They added a box-filtered downscale in Java beside the platform's own scaling, on the grounds that two ports point-sample. That is not the layer to fix it in: scaling belongs to the implementation code, drawing a scaled image is fast on every device the framework targets, and scaled() remains for legacy callers. The runtime's scaled-copy path now uses scaled()/fill() like everything else. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
They were opened up so the Cupertino picker render elements could reach them from another package, and went out with one-line comments that said little more than the class name. Each now says what it selects, that it is an ordinary Container usable outside a Picker dialog, and -- the part a caller cannot guess -- the runtime TYPE that getValue and setValue carry: a Date for the date spinners, minutes since midnight as an Integer for TimeSpinner3D, milliseconds as a Long for DurationSpinner3D, the selected model element for Spinner3D. Every one of those was read off the implementation rather than assumed; DateSpinner3D in particular preserves the time of day of whatever was last set, which is worth knowing before round-tripping a timestamp through it. InternalPickerWidget gains the contract itself, and a note on why an interface named Internal is public. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The workflow cloned `stable`, which is a moving target: CI picked up 3.47.5 while every number in this branch was measured against 3.35.4. That is a defect in a benchmark on its own -- the gallery, its dependency resolution and Flutter's own code generation all change between releases, so two runs a week apart would differ for reasons unrelated to this repository. It also broke outright. 3.47.5's tree resolved google_fonts to a version without robotoCondensed, and every platform job failed compiling three of the studies -- the single root cause behind the ios, android, linux, windows and javascript failures on the previous run. Pinned to 3.35.4, verified to carry what prepare.sh needs: the 159-file gallery, a root pubspec.lock, and google_fonts 6.2.1. The gallery check now also asserts that lockfile is present, since without it the failure surfaces much later as a missing member on a font. Bumping FLUTTER_REF re-baselines the comparison and should re-record baselines/ in the same change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…to build in Five unrelated-looking failures with one shape each: The migration and demo suites generate a project from the archetype and then build it, but never named a javaVersion, so a JDK 8 leg asked for release 17 and failed inside a project the developer never wrote. tests/ and the archetype tests already derive it; these six had been missed. Derived the same way rather than pinned, so each leg tests a pairing that can exist. The Flutter benchmark's Codename One build opens an AWT window -- its CSS compiler -- so every Linux runner died in cn1:css *after* the transpile had already succeeded, which reads like a Flutter problem and is not one. Wrapped Maven in xvfb-run at cn1_build, the single point every platform recipe goes through, and made a missing xvfb fail loudly instead of later and less legibly. -Djava.awt.headless=true is not the fix: the toolchain genuinely uses AWT. prepare.sh generated the archetype under -q, so when the Windows leg failed it printed an exit code and no reason at all. A build step that cannot say why it failed is worse than a noisy one. Maven records a failed download as a *.lastUpdated marker and answers later resolutions from it, so retry.sh was re-running commands against a cache that could only give the same answer -- four attempts, all of them offline, and with cache: maven the poisoning outlived the run that caused it. retry.sh now clears those markers between attempts; they record only failures, never artifacts. The developer guide's LanguageTool gate counts matches, and the new chapter was the only place in the guide spelling it rasteriser/penalises/analogue while the other nine hundred lines say rasterizer/analog. Matched the guide instead of widening the accept list; "transpiled" is a real term of art, so that one is declared beside "transpiler". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
With xvfb in place the Linux leg got past cn1:css and reached what it was always meant to exercise -- the native local-linux-device build -- and stopped in CMake on "Package 'libcurl', required by 'virtual:world', not found". The job installed an ad-hoc five-package subset. The port's CMakeLists resolves its whole stack through pkg_check_modules and names only the first package missing, so trimming that list by trial and error costs one CI round trip per package, several minutes into a build that has already transpiled and compiled. linux-build-run.yml already carried the correct list, with the reasoning for each group and for why the GStreamer plugin set is part of it. Both jobs build the same target, so the list is now one script that each calls, rather than a second copy to drift from the first -- the drift that retry.sh's own comments record happening three times. Callers needing extra packages pass them as arguments; the benchmark passes clang and libstdc++ for the Flutter side's own C++ build. Verified the moved list is byte-identical: the same 30 packages. Also: prepare.sh and build_apps.sh now report the line they failed on. The Windows leg has twice reported an exit code and nothing else -- once because Maven ran under -q, and once, with -q removed, from a command that genuinely prints nothing on failure -- and my first guess about which command that was turned out to be wrong. -E so the trap is inherited by the subshells the build steps run in. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
build-test (17) and (21) failed on MenuBarDialogSideMenuTest, which passed on
the previous commit of this branch, passes on every developer machine, and was
not touched by anything in between.
TestCodenameOneImplementation.touchDevice defaults to TRUE. DialogInWindowTest
turns it on for a keyboard assertion and then, in its finally, "restores" it to
a hardcoded false -- not to what was there. reset() clears its sibling
device-shape flags, deviceDensity among them, but never this one, so the wrong
value survives into every later test in that JVM.
The leak is silent, which is what made it hard to place: nothing fails where it
happens. The next test to ask for COMMAND_BEHAVIOR_BUTTON_BAR has it quietly
rewritten to COMMAND_BEHAVIOR_SOFTKEY by
CodenameOneImplementation.setCommandBehavior, its commands are drawn as soft
buttons, and it fails several tests later on a missing button bar with nothing
naming the cause.
And it is a lottery rather than a flake. Surefire's filesystem run order differs
per checkout, so whether the leaking test runs first is luck -- on macOS the
MenuBar test runs before DialogInWindowTest, in CI it runs after, and the same
code therefore passed one run and failed the next.
Fixed at the choke point: reset() restores the flag, so every test is covered
rather than the one that happened to be caught. DialogInWindowTest also puts
back the value it found, because a finally that restores a constant is wrong
however the flag is cleaned up elsewhere.
Reproduced before fixing and verified after, with the order CI happened to pick:
mvn -DunitTests -pl core-unittests test -Dsurefire.runOrder=alphabetical \
-Dtest='DialogInWindowTest,MenuBarDialogSideMenuTest'
fails with CI's exact assertion beforehand and passes afterwards.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The macOS leg translated and compiled the whole application and then stopped in Xcode on "Signing for Bench requires selecting a development team". MacOSBuildHints defaults both signing identities to a real certificate rather than leaving them null -- deliberately, so that a project carrying only a team id cannot silently produce an unsigned artifact somebody paid to ship. A build that configures nothing therefore still tries to sign. `none` is the sentinel the builder documents for asking not to, and with both channels set it passes CODE_SIGNING_ALLOWED=NO to xcodebuild, which is what the ios recipe alongside already does by hand. Both channels, because the two default independently. Nothing here is distributed, so there is nothing to sign for: the binary is measured and thrown away, and Flutter's half is unsigned too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Windows leg has now failed three times with an exit code and no message.
The ERR trap added last commit put it on line 242:
ASSETS="$(find "${PUB_CACHE:-$HOME/.pub-cache}/hosted" ... 2>/dev/null | ...)"
~/.pub-cache is the default on Linux and macOS only; Windows keeps the cache
under LOCALAPPDATA. So find searched a directory that does not exist and exited
non-zero, its stderr went to /dev/null, and pipefail turned the empty assignment
into the death of the script -- silent twice over, which is why two earlier
guesses about which command was failing were both wrong.
`flutter pub get` already writes .dart_tool/package_config.json, which names
where every resolved package actually lives on every platform, so the location
is no longer guessed at all. The file URI is parsed by hand rather than through
url2pathname, which is a different function per platform: the POSIX build
returns "/C:/Users/..." for a Windows URI, leading slash and all, so testing
that on a Mac would have proved nothing about the platform it is for. Verified
against a real package_config.json from a prepared tree, and on synthesised
POSIX, Windows, percent-encoded and relative-rootUri inputs.
Missing assets are now FATAL rather than a warning. A build with no artwork is
a smaller build, so the warning would have reported an installed size
flattering to Codename One and not comparable with Flutter's -- the exact class
of quietly unfair number the rest of this harness exists to prevent. That is not
hypothetical: a work tree prepared before the workspace-lifting fix declares
packages/flutter_gallery_assets/... in its pubspec and does not carry the
package in package_config.json at all, and the old code would have measured it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…is missing
The Windows leg now gets both applications prepared and fails building the
FLUTTER half:
CMake Error at CMakeLists.txt:3 (project):
Generator "Visual Studio 16 2019" could not find any instance of Visual
Studio.
That generator is Flutter's fallback for detecting NO Visual Studio at all --
it picks the generator from the install it finds, so the message names a
version nobody asked for and says nothing about the real fault. The runner
certainly has some Visual Studio; the question is why Flutter does not accept
it, and the two candidates need different fixes: an install missing the C++
components, or an install NEWER than the pinned Flutter knows how to map.
This prints vswhere's view beside flutter doctor's so the two can be compared,
and fails there rather than minutes later in CMake. A benchmark should record
the toolchain it measured with in any case -- the Flutter revision already is.
Deliberately a probe, not a guess at the fix: three earlier guesses at why this
platform was failing were all wrong, and each cost a full CI round trip.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Compiles Dart widget source to Java at build time and runs it on Codename One's own renderer. There is no Dart VM in the result, no embedded engine and no platform view — a Dart screen becomes ordinary Codename One components, so it inherits the theme, the event thread, the accessibility tree and the native build.
Two entry points, which are the two things a user actually wants to do:
FlutterUI.wrap(widget)returns aContainerthat goes anywhere an ordinary component does, for putting one Dart screen inside an existing app. It deliberately does not install the Material base theme, so it will not restyle the screen around it.FlutterUI.runApp(widget)mounts the tree as the whole UI, and does install it.The build wiring is the archetype's own: the
transcode-fluttergoal is already bound and is a silent no-op untilsrc/main/flutterexists. No Dart SDK is involved — the transpiler is Java and the Dart is input, never executed.Benchmark
scripts/flutter-benchbuilds one application two ways — the Flutter toolchain's release build, and the identical Dart source transpiled by Codename One — and publishes size, start-up and idle memory per platform to the PR and to port status.Neither application is vendored.
prepare.shtakes the gallery from the Flutter SDK CI clones and copies the same 159 files into both trees, so the comparison cannot drift: there is no second copy for an edit to land on. An earlier round of this work compared two different galleries and every number it produced was meaningless. The Codename One side is generated from the shipping archetype with the runtime dependency enabled — the two steps the guide tells a user to take — so a change that breaks the documented wiring breaks the benchmark too.Three measurement decisions, each because the obvious alternative flattered us:
FIRSTCONTENTis a UI-thread callback that runs before that frame is rasterised, while ourFIRSTFRAMEfires once the form is on screen. Comparing them charges one runtime for rasterising its first screen and not the other — which is what the harness this replaces did. Flutter's figure is reported as a range and the ratio uses the end least favourable to us.Runneris a thin launcher and the Dart image sits inFrameworks/App.framework. Sizing the executable compared our whole runtime against their stub.And two refusals rather than a substituted number:
Nothing contacts the build server: the
*-sourceandlocal-*targets build on the runner. Only our own numbers are gated, so a Flutter SDK upgrade that grows their build cannot turn ours red.Fidelity work in this PR
RoundBorderdraws itself when there is no shadow and nouiidmode, instead of going through a component-sized offscreen image. The cache hides the cost for a static shape; for one that animates its size it does not — a circle growing to 1618×1618 threw away a 10MB surface per frame.Component.paintInternalImplclips every component to its own rectangle, soFractionalTranslationdrew most of itself into the discarded region — the feature-discovery circle rendered as a quadrant with two straight edges meeting at its centre.FittedBoxwas a pass-through. Flutter lays its child out unbounded and scales the result, which is why text inside one shrinks instead of wrapping.Cardrebuilt its border every time, andRoundRectBordercaches its shadow against the border instance, so every rebuild re-rendered it with a gaussian blur. With the cache off it also translates the liveGraphicsby the shadow offset and never undoes it, so cards drifted 9px down and 4px across, accumulating down the page.Verification
/demo/card14.98% → 2.30%,/demo/grid-lists26.30% → 0.32%prepare.shverified end to end; the generated project builds 563 Java files from 159 Dart files, which also exercises the documented user pathNot yet proven
No benchmark platform adapter has been exercised end to end —
run_bench.py --listreports that rather than implying otherwise, and the first CI run is the thing to review. The native compile steps (xcodebuild, gradle, clang-cl, GTK3) have not run on a runner.This is also the first mention of the feature in
docs/, which has carried none until now.