Skip to content

bug: Provide a way to preserve widget State across layout rebuilds where the parent widget type changes (e.g. inside ResponsiveRow) #6661

Description

@bl1nch

Duplicate Check

Describe the bug

When a layout container rebuilds its subtree with a different root widget type between two builds (for example, ResponsiveRowControl.build() returns either a Row or a Wrap depending on whether the resolved col values for the current breakpoint fit within columns), Flutter's element reconciliation cannot reuse the old Elements: since the runtimeType at that slot changed, the whole subtree below it is unmounted and a new one is inflated from scratch, regardless of the keys used by descendant widgets.

How a layout control chooses to rearrange its children (Row vs Wrap, or any other conditional layout change) is its own logic and design choice.
The point here is the side effect: any descendant control that keeps expensive or native state in its Flutter State (an open media player, a WebView, animation/scroll state, etc.) gets that state fully disposed and recreated whenever this kind of type-changing rebuild happens above it.

Concretely, with flet_video's VideoControl, _teardown() (disposing the media_kit Player/VideoController) followed by _setup() (creating a new Player and reopening the playlist) runs on every such rebuild, producing a visible black-screen flash and playback restart.

This isn't unique to ResponsiveRow — it's a generic consequence of Flutter's reconciliation rules, so it likely shows up anywhere in Flet where a control conditionally changes the widget type it returns from build() (not just Row/Wrap — any conditional branching that changes the top-level widget type).

Code sample

Code
import flet as ft
import flet_video as ftv


async def main(page: ft.Page):
    media = "https://user-images.githubusercontent.com/28951144/229373720-14d69157-1a56-4a78-a2f4-d7a134d7c3e9.mp4"

    page.add(
        ft.ResponsiveRow(
            controls=[
                ftv.Video(
                    expand=True,
                    autoplay=True,
                    playlist=[ftv.VideoMedia(media)],
                    aspect_ratio=16/9,
                    col={ft.ResponsiveRowBreakpoint.MD: 6},
                ),
                ft.Container(
                    expand=True,
                    height=150,
                    bgcolor=ft.Colors.GREEN_ACCENT_100,
                    col={ft.ResponsiveRowBreakpoint.MD: 6},
                ),
            ]
        )
    )


if __name__ == "__main__":
    ft.run(main)

To reproduce

  1. Run the sample above (requires flet and flet-video).
  2. Resize the window/browser so its width repeatedly crosses the MD breakpoint, so the two children switch back and forth between sitting side by side and stacking on top of each other.
  3. Observe that each time this switch happens, the Video control goes black for a moment and restarts playback from the beginning of the currently open media, instead of continuing to play uninterrupted.

Expected behavior

A way to opt a specific control into preserving its Flutter State across a rebuild that only changes layout, without changing the control itself.

Screenshots / Videos

Captures

[Upload media here]

Operating System

Windows

Operating system details

11

Flet version

0.85.3

Regression

No, it isn't

Suggestions

Flutter's GlobalKey lets an existing Element (and its State) be reused/reparented into a new location in the tree within the same build pass, even when the immediate ancestor's widget type changed — which is what's needed here.

This probably shouldn't apply to every control by default. A per-control opt-in flag (e.g. preserve_state on Control) could work: when set, Flet would build/reuse a GlobalKey for that control's widget — something like GlobalObjectKey(control.id), cached rather than recreated on every build.

The cache for these keys could live on flet.View (ViewControl), since navigating away from a View and disposing it is an expected point for state to reset, so the cache wouldn't need to survive across views — only across rebuilds within the same View.

Logs

Logs
[Paste your logs here]

Additional details

No response

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

Status
✅ Done

Relationships

None yet

Development

No branches or pull requests

Issue actions