Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
27c4c56
initial commit
ndonkoHenri May 5, 2026
d9f276b
update test
ndonkoHenri May 5, 2026
0bd5a72
update changelog
ndonkoHenri May 5, 2026
f1929be
update changelog with PR number
ndonkoHenri May 5, 2026
409fab6
fix #6443: improve `LineChart` event spot serialization
ndonkoHenri May 5, 2026
5e18780
update changelogs and add dev-testing-skill
ndonkoHenri May 5, 2026
0bbec85
fix failing CI
ndonkoHenri May 5, 2026
1edca06
fix flaky screenshot capture: await endOfFrame before toImage()
FeodorFitsner May 6, 2026
342eaca
Merge branch 'release/v0.85.0' into fix-nav-destination-selected-icon
FeodorFitsner May 6, 2026
8a11c5f
test(row): pin viewport in test_alignment / test_vertical_alignment
FeodorFitsner May 6, 2026
f632488
fix #6429: update tile layer URL to OpenStreetMap
ndonkoHenri May 6, 2026
a62612f
test: bump screenshot capture delay to 200ms
FeodorFitsner May 6, 2026
2c9ef31
ci: bump test log level to DEBUG (temp)
FeodorFitsner May 6, 2026
0e3228e
test(row): pump_and_settle after resize_page
FeodorFitsner May 6, 2026
1285f55
test(row): revert resize_page additions
FeodorFitsner May 6, 2026
3213c10
test(row): pump 2s before test_alignment capture
FeodorFitsner May 6, 2026
08e15f5
example(row/alignment): drop redundant inner Column scroll
FeodorFitsner May 6, 2026
0d61e6b
revert diagnostic-only changes
FeodorFitsner May 6, 2026
5af6475
fix(scrollable_control): use MediaQuery.sizeOf instead of LayoutBuilder
FeodorFitsner May 6, 2026
2a63caf
fix(scrollable_control): replace LayoutBuilder with paired RenderProx…
FeodorFitsner May 6, 2026
f8cd952
fix(scrollable_control): mark inner enforcer dirty on outer constrain…
FeodorFitsner May 6, 2026
dfcf014
ci: restore full integration test matrix
FeodorFitsner May 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions .agents/skills/test-flet-apps-dev/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
---
name: test-flet-apps-dev
description: Use when testing or debugging Flet apps in maintainer/contributor development mode with local Python package sources and the local Flutter client, including web, desktop, browser, and computer-use verification workflows.
---

# Test Flet Apps In Dev Mode

Use this skill when validating a Flet app, example, feature, or bug fix against
this repo's local Python packages and/or local Flutter client during maintainer
or contributor work.

## Core model

Flet dev-mode testing usually has one or two processes:

1. A Python Flet app/server, run from `sdk/python`.
2. The local Flutter client, run from `client`, when Dart/client or extension code
must be tested.

If only Python code changed, `uv run flet run ...` is often enough. If Dart,
Flutter, extension, or transport code changed, run the local Flutter client so
the changed Dart code is actually used.

The local Flutter client debug build uses a fixed app-server URL from
`client/lib/main.dart`:

```dart
if (kDebugMode) {
pageUrl = "http://localhost:8550";
}
```

Therefore, when using the local Flutter client without extra URL arguments,
start the Python app on port `8550`.

## Start the Python app

Run from `{repo}/sdk/python`:

```bash
uv run flet run -w -p 8550 examples/controls/core/interactive_viewer/handling_events/main.py
```

Adjust the sample path as needed. Use port `8550` when the local Flutter client
will connect with its default debug URL. Keep this process running and watch its
stdout for Python callback output, tracebacks, and event payloads.

For web-only checks with the packaged web client, open:

```text
http://127.0.0.1:8550
```

## Run the local Flutter client

Run these from `{repo}/client`.

### Desktop

Use when validating native desktop behavior on the current host OS:

```bash
fvm flutter run -d macos # macOS
fvm flutter run -d windows # Windows
fvm flutter run -d linux # Linux
```

The debug client defaults to `http://localhost:8550` when no app URL argument is
provided. Use the platform target that exists on the current machine. Use
Computer Use or the relevant platform automation to navigate and interact with
the app window.

### Flutter web in Chrome

Use when Dart web behavior must be validated in Flutter's default web debug
browser:

```bash
fvm flutter run -d chrome
```

This opens a fresh browser connected to the Python app server on port `8550`.

### Flutter web in another Chromium browser

If the requested browser is not listed by `flutter devices`, prefer the web
server target and open the served URL in that browser:

```bash
fvm flutter run -d web-server --web-hostname 127.0.0.1 --web-port 8660
open -a "Brave Browser" http://127.0.0.1:8660
```

Using `CHROME_EXECUTABLE` can work, but Flutter may fail to attach its debug
websocket in non-default Chromium browsers. Fall back to `web-server` if that
happens.

## Browser and UI interaction

- For local browser targets (`localhost`, `127.0.0.1`, `file://`), prefer the
in-app browser or the Browser Use plugin when explicitly requested.
- Use Computer Use for native desktop apps and external browsers when browser
MCP is not the requested tool or cannot control that browser.
- For chart/canvas-heavy UI, click/hover coordinates may be necessary because
accessibility trees often expose only the HTML canvas container.

## Reading evidence

Always inspect both sides:

- Python app stdout: event payloads, user `print()` calls, Python tracebacks.
- Flutter run stdout: client-side event payloads, WebSocket messages, Flutter
exceptions, hot reload/restart status.
- Browser/app state: the actual rendered UI and any visible error banner.

For client/server protocol bugs, compare the raw outgoing Dart event in Flutter
logs with the decoded Python event object in Python logs.

## Hot reload and restart

For Flutter client sessions:

- Press `r` for hot reload after many Dart-only edits.
- Press `R` for hot restart if state, initialization, or extension registration
may be stale.
- Quit with `q` before final response unless the user explicitly wants the app
left running.
- Press `h` for help on other Flutter run key commands.

For Python app sessions, restart `uv run flet run ...` after Python source or
sample changes if the running process does not pick them up.

## Troubleshooting

- If a sandboxed Flutter command fails trying to write FVM or Flutter cache files
such as `engine.stamp`, rerun the same command with escalation.
- If `flutter devices` does not list Brave/Edge/etc., use `flutter run -d web-server`
and open the URL in the target browser.
- If the UI shows a generic Flet error banner, check Python stdout first; the
root cause is often a handler exception.
- If an event handler indexes a list payload, confirm the empty-list case before
treating it as a framework bug.
- If the local Flutter client cannot connect, confirm the Python app is running
on port `8550` or pass an explicit app URL when the client path supports it.

## Finish checklist

- Stop long-running app/test sessions unless asked to leave them running.
- State exactly which surfaces were tested: packaged web, local Flutter web,
desktop target, target browser, or sample-only.
- Include the key observed payload/error before and after the fix.
- Separate framework bugs from sample-code guard issues.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
* Fix `Page.on_resize` and `Page.on_media_change` not firing after mobile orientation changes ([#6457](https://github.com/flet-dev/flet/issues/6457), [#6423](https://github.com/flet-dev/flet/pull/6423)) by @ndonkoHenri.
* Fix `flet pack` desktop packaging so Windows and Linux bundles include the expected client archive, and Windows taskbar pins point to the packed app instead of the cached `flet.exe` ([#5151](https://github.com/flet-dev/flet/issues/5151), [#6403](https://github.com/flet-dev/flet/pull/6403)) by @ndonkoHenri.
* Fix environment variable priority in `flet build` template: inherit from `Platform.environment` and use `putIfAbsent` for FLET_* variables so pre-set system env vars are not overwritten ([#6394](https://github.com/flet-dev/flet/pull/6394)) by @Bahtya.
* Fix `NavigationBarDestination.selected_icon` rendering wrongly when provided as an `Icon` control ([#6460](https://github.com/flet-dev/flet/issues/6460), [#6468](https://github.com/flet-dev/flet/pull/6468)) by @ndonkoHenri.
* Fix 3- and 4-digit hex color shorthand (e.g. `#c00`, `#fc00`) rendering as invisible by expanding them to their full 6/8-digit forms ([#6419](https://github.com/flet-dev/flet/issues/6419), [#6421](https://github.com/flet-dev/flet/pull/6421)) by @ndonkoHenri.
* Fix `LineChartEvent.spots` returning undecoded MessagePack extension values instead of `LineChartEventSpot` objects ([#6443](https://github.com/flet-dev/flet/issues/6443), [#6468](https://github.com/flet-dev/flet/pull/6468)) by @ndonkoHenri.
* Fix `LineChart` (and other charts) silently dropping custom `ChartAxisLabel` entries whose `value` matched a tick only after floating-point rounding (e.g. `0.1`, `0.2`, `0.3`) by switching label lookup to a tolerance-based comparison scaled to the axis interval ([#6445](https://github.com/flet-dev/flet/issues/6445), [#6459](https://github.com/flet-dev/flet/pull/6459)) by @KangZhaoKui.
* Fix absolute-path `src` (e.g. `Image(src="/images/foo.svg")`) breaking on web when the app is mounted at a non-root URL, pass `data:`/`blob:` URIs through the asset resolver unchanged, preserve origin-relative semantics when `assets_dir` is unset, and add a `window.flet.assetsDir` JS-interop bridge so embedding hosts can supply `assets_dir` to the top-level `FletApp` ([#6470](https://github.com/flet-dev/flet/pull/6470)) by @FeodorFitsner.

Expand Down
14 changes: 6 additions & 8 deletions packages/flet/lib/src/controls/navigation_bar_destination.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import 'package:flutter/material.dart';

import '../extensions/control.dart';
import '../models/control.dart';
import '../utils/icons.dart';
import '../utils/numbers.dart';
import 'base_controls.dart';

Expand All @@ -15,14 +14,13 @@ class NavigationBarDestinationControl extends StatelessWidget {
Widget build(BuildContext context) {
debugPrint("NavigationBarDestination build: ${control.id}");

var selectedIcon = control.getIconData("selected_icon");
var child = NavigationDestination(
enabled: !control.disabled,
tooltip: !control.disabled ? control.getString("tooltip") : null,
icon: control.buildIconOrWidget("icon")!,
selectedIcon: control.buildWidget("selected_icon") ??
(selectedIcon != null ? Icon(selectedIcon) : null),
label: control.getString("label", "")!);
enabled: !control.disabled,
tooltip: !control.disabled ? control.getString("tooltip") : null,
icon: control.buildIconOrWidget("icon")!,
selectedIcon: control.buildIconOrWidget("selected_icon"),
label: control.getString("label", "")!,
);

return BaseControl(control: control, child: child);
}
Expand Down
164 changes: 139 additions & 25 deletions packages/flet/lib/src/controls/scrollable_control.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

import '../models/control.dart';
import '../utils/animations.dart';
Expand Down Expand Up @@ -32,6 +33,7 @@ class _ScrollableControlState extends State<ScrollableControl>
with FletStoreMixin {
late final ScrollController _controller;
late bool _ownController = false;
final _ConstraintsHolder _outerConstraints = _ConstraintsHolder();

@override
void initState() {
Expand Down Expand Up @@ -111,34 +113,29 @@ class _ScrollableControlState extends State<ScrollableControl>

Widget child = widget.child;
if (widget.wrapIntoScrollableView) {
child = LayoutBuilder(builder: (context, constraints) {
final minWidth = widget.scrollDirection == Axis.horizontal &&
constraints.hasBoundedWidth
? constraints.maxWidth
: 0.0;
final minHeight = widget.scrollDirection == Axis.vertical &&
constraints.hasBoundedHeight
? constraints.maxHeight
: 0.0;

Widget scrollViewChild = widget.child;
if (minWidth > 0 || minHeight > 0) {
scrollViewChild = ConstrainedBox(
constraints:
BoxConstraints(minWidth: minWidth, minHeight: minHeight),
child: scrollViewChild,
);
}

return SingleChildScrollView(
controller: _controller,
// The pre-#6450 path used a plain SingleChildScrollView. PR #6450 added
// a LayoutBuilder + ConstrainedBox(minHeight: parentMaxHeight) wrapper
// so vertical alignment works in scrollable Page/View when content is
// shorter than the viewport. LayoutBuilder, however, reports 0 for
// intrinsic dimensions, which collapses any ancestor IntrinsicWidth /
// IntrinsicHeight and leaves the layout perpetually dirty.
//
// Replicate the behavior with two cooperating RenderProxyBoxes that
// forward intrinsic queries to their child. The outer reader captures
// the parent's constraints during performLayout; the inner enforcer
// reads them back and applies them as a min on the scroll-view child.
child = SingleChildScrollView(
controller: _controller,
scrollDirection: widget.scrollDirection,
child: _InnerConstraintsEnforcer(
holder: _outerConstraints,
scrollDirection: widget.scrollDirection,
child: scrollViewChild,
);
});
child: widget.child,
),
);
}

return Scrollbar(
Widget result = Scrollbar(
thumbVisibility: scrollConfiguration.thumbVisibility,
trackVisibility: scrollConfiguration.trackVisibility,
thickness: scrollConfiguration.thickness,
Expand All @@ -150,5 +147,122 @@ class _ScrollableControlState extends State<ScrollableControl>
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
child: child,
));

if (widget.wrapIntoScrollableView) {
result = _OuterConstraintsReader(
holder: _outerConstraints,
child: result,
);
}

return result;
}
}

/// Carries box constraints from [_OuterConstraintsReader] (outside the scroll
/// view) to [_InnerConstraintsEnforcer] (inside it) within a single layout
/// pass. Holds a reference to the inner enforcer so the outer reader can
/// mark it dirty when its incoming constraints change — without that, the
/// inner enforcer would skip re-layout on window resize because the
/// constraints it sees from SingleChildScrollView (unbounded in scroll axis)
/// don't change.
class _ConstraintsHolder {
BoxConstraints? value;
RenderObject? listener;
}

class _OuterConstraintsReader extends SingleChildRenderObjectWidget {
const _OuterConstraintsReader({required this.holder, super.child});
final _ConstraintsHolder holder;

@override
RenderObject createRenderObject(BuildContext context) =>
_RenderOuterConstraintsReader(holder);

@override
void updateRenderObject(
BuildContext context, _RenderOuterConstraintsReader renderObject) {
renderObject.holder = holder;
}
}

class _RenderOuterConstraintsReader extends RenderProxyBox {
_RenderOuterConstraintsReader(this.holder);
_ConstraintsHolder holder;

@override
void performLayout() {
final changed = holder.value != constraints;
holder.value = constraints;
if (changed && holder.listener != null) {
// Force the inner enforcer to re-run performLayout in this layout pass.
// invokeLayoutCallback enables mutations during layout — without it,
// markNeedsLayout asserts.
invokeLayoutCallback<BoxConstraints>((_) {
holder.listener?.markNeedsLayout();
});
}
super.performLayout();
}
}

class _InnerConstraintsEnforcer extends SingleChildRenderObjectWidget {
const _InnerConstraintsEnforcer({
required this.holder,
required this.scrollDirection,
super.child,
});
final _ConstraintsHolder holder;
final Axis scrollDirection;

@override
RenderObject createRenderObject(BuildContext context) =>
_RenderInnerConstraintsEnforcer(holder, scrollDirection);

@override
void updateRenderObject(
BuildContext context, _RenderInnerConstraintsEnforcer renderObject) {
renderObject
..holder = holder
..scrollDirection = scrollDirection;
}
}

class _RenderInnerConstraintsEnforcer extends RenderProxyBox {
_RenderInnerConstraintsEnforcer(this.holder, this.scrollDirection);
_ConstraintsHolder holder;
Axis scrollDirection;

@override
void attach(PipelineOwner owner) {
super.attach(owner);
holder.listener = this;
}

@override
void detach() {
if (holder.listener == this) holder.listener = null;
super.detach();
}

@override
void performLayout() {
if (child == null) {
size = computeSizeForNoChild(constraints);
return;
}
BoxConstraints childConstraints = constraints;
final outer = holder.value;
if (outer != null) {
if (scrollDirection == Axis.vertical && outer.hasBoundedHeight) {
childConstraints =
childConstraints.copyWith(minHeight: outer.maxHeight);
} else if (scrollDirection == Axis.horizontal && outer.hasBoundedWidth) {
childConstraints =
childConstraints.copyWith(minWidth: outer.maxWidth);
}
}
child!.layout(childConstraints, parentUsesSize: true);
size = child!.size;
}
}
4 changes: 3 additions & 1 deletion sdk/python/examples/extensions/map/basic/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ def main(page: ft.Page):
expand=True,
layers=[
ftm.TileLayer(
url_template="https://tile.memomaps.de/tilegen/{z}/{x}/{y}.png",
url_template="https://tile.openstreetmap.org/{z}/{x}/{y}.png",
user_agent_package_name="flet-map-examples/1.0",
on_image_error=lambda e: print(f"TileLayer Error: {e.data}"),
),
ftm.SimpleAttribution(text="OpenStreetMap contributors"),
],
),
)
Expand Down
Loading
Loading