website/blog/2026-05-11-flet-v-0-85-release-announcement.md
Flet 0.85.0 brings first-class declarative navigation and dialog management, richer media controls, and a long list of bug fixes.
Highlights in this release:
ft.Router for @ft.component apps — nested routes, layouts with outlets, dynamic segments, data loaders, and manage_views=True for native view-stack navigation.ft.use_dialog() hook — dialogs are now reactive state in declarative apps, not imperative page.show_dialog() calls.flet-video: configurable controls, Video.take_screenshot(), and on_position_change / on_duration_change events.AudioRecorder PCM16 streaming via on_stream chunks and direct upload through AudioRecorderUploadSettings.If you use pip:
pip install 'flet[all]' --upgrade
If you use uv with pyproject.toml and want to upgrade everything:
uv sync --upgrade
If you want to upgrade only Flet packages:
uv sync --upgrade-package flet \
--upgrade-package flet-cli \
--upgrade-package flet-desktop \
--upgrade-package flet-web
Imperative Flet apps have always had Page.route and Page.views for navigation. But declarative apps — the ones built around @ft.component — had to roll their own: subscribe to route changes, parse the path, render the right component. It worked, but it was boilerplate that every app reinvented.
0.85.0 adds ft.Router: a declarative, React Router-style component that matches the current page route against a tree of ft.Route definitions and renders the matched component chain. Here's the simplest possible example:
import flet as ft
@ft.component
def Home():
return ft.Text("Home page", size=24)
@ft.component
def About():
return ft.Text("About page", size=24)
@ft.component
def App():
return ft.SafeArea(
content=ft.Column([
ft.Row([
ft.Button("Home", on_click=lambda: ft.context.page.navigate("/")),
ft.Button("About", on_click=lambda: ft.context.page.navigate("/about")),
]),
ft.Router([
ft.Route(index=True, component=Home),
ft.Route(path="about", component=About),
]),
])
)
ft.run(lambda page: page.render(App))
Routes can nest, and a parent route can render a shared layout that wraps its children using ft.use_route_outlet():
@ft.component
def AppLayout():
outlet = ft.use_route_outlet()
return ft.Column([
ft.Container(
content=ft.Row([
ft.Text("My App", size=20, weight=ft.FontWeight.BOLD),
ft.Button("Home", on_click=lambda: ft.context.page.navigate("/")),
ft.Button("About", on_click=lambda: ft.context.page.navigate("/about")),
]),
bgcolor=ft.Colors.SURFACE_BRIGHT,
padding=10,
),
ft.Container(content=outlet, padding=20),
])
@ft.component
def App():
return ft.Router([
ft.Route(component=AppLayout, children=[
ft.Route(index=True, component=Home),
ft.Route(path="about", component=About),
]),
])
What Router supports:
outlet=True and ft.use_route_outlet()./users/:id and optional segments like /posts/:id?./files/*).manage_views=True — switches the router into view-stack mode where each route returns a full View with its own AppBar. Navigating deeper pushes views onto the stack, and the user can swipe back or tap the AppBar back button on mobile.More info:
use_dialog() hookDialogs in imperative Flet are imperative: you call page.show_dialog(...) to open, page.close_dialog() to close. That model doesn't fit declarative apps, where the UI is supposed to be a function of state. Until now, the workaround was to keep a reference to the dialog and toggle open manually — fiddly and easy to get wrong.
The new ft.use_dialog() hook closes that gap. Pass a DialogControl to show it, pass None to dismiss it. The dialog is portaled into the page's dialog overlay automatically, and removed when the component unmounts:
import asyncio
import flet as ft
@ft.component
def App():
show, set_show = ft.use_state(False)
deleting, set_deleting = ft.use_state(False)
async def handle_delete():
set_deleting(True)
await asyncio.sleep(2)
set_deleting(False)
set_show(False)
ft.use_dialog(
ft.AlertDialog(
modal=True,
title=ft.Text("Delete report.pdf?"),
content=ft.Text(
"Deleting, please wait..." if deleting else "This cannot be undone."
),
actions=[
ft.Button(
"Deleting..." if deleting else "Delete",
disabled=deleting,
on_click=handle_delete,
),
ft.TextButton(
"Cancel",
on_click=lambda: set_show(False),
disabled=deleting,
),
],
on_dismiss=lambda: set_show(False),
)
if show
else None
)
return ft.Button("Delete File", icon=ft.Icons.DELETE, on_click=lambda: set_show(True))
A subtle but important detail: the hook uses frozen-diff reactive updates. When the component re-renders and you pass back a new dialog instance with different field values, the hook diffs it field-by-field against the previous instance and emits only the actual deltas — instead of replacing the dialog wholesale. That means a TextField inside an AlertDialog keeps its cursor, focus, and selection across re-renders, even though Python is handing the framework a brand-new control object on every build.
You can also call use_dialog() multiple times in the same component to manage independent dialogs (e.g. a rename dialog and a delete dialog on the same screen), and each one is tracked separately.
More info:
flet-video got a substantial upgrade. The control bar is now fully configurable: you can use the built-in controls, replace them with your own widgets, hide them entirely, or specify different controls for normal and fullscreen modes. There's also a new Video.take_screenshot() method for capturing the currently displayed frame, and two new events for keeping your UI in sync with playback:
on_position_change — fires as playback progresses, useful for driving a custom progress bar.on_duration_change — fires when the video's duration becomes known (or changes between playlist entries).async def handle_screenshot(e):
image_bytes = await video.take_screenshot()
# save, upload, display in an Image, etc.
video = ft.Video(
playlist=[ft.VideoMedia("https://example.com/clip.mp4")],
on_position_change=lambda e: print(f"At {e.position}s"),
on_duration_change=lambda e: print(f"Duration: {e.duration}s"),
)
More info:
Until 0.85.0, AudioRecorder recorded to a file and you read the file back when you were done. That's fine for "record then transcribe" flows, but it doesn't work for real-time use cases — voice activity detection, live transcription, streaming an LLM voice assistant.
Now AudioRecorder can stream raw PCM16 chunks via the new on_stream event as audio is captured. Here's the core of the streaming example — receive chunks, buffer them, and write a WAV when recording stops:
import flet as ft
import flet_audio_recorder as far
def main(page: ft.Page):
buffer = bytearray()
def handle_stream(e: far.AudioRecorderStreamEvent):
buffer.extend(e.chunk)
status.value = f"Streaming chunk {e.sequence}; {e.bytes_streamed} bytes."
async def start(e):
buffer.clear()
await recorder.start_recording(
configuration=far.AudioRecorderConfiguration(
encoder=far.AudioEncoder.PCM16BITS,
sample_rate=44100,
channels=1,
),
)
recorder = far.AudioRecorder(on_stream=handle_stream)
page.add(ft.Button("Start", on_click=start), status := ft.Text())
For server-side capture without buffering through Python, point the recorder at an upload URL and the audio uploads as it streams:
upload_url = page.get_upload_url(file_name="rec.pcm", expires=600)
await recorder.start_recording(
upload=far.AudioRecorderUploadSettings(upload_url=upload_url, file_name="rec.pcm"),
configuration=far.AudioRecorderConfiguration(encoder=far.AudioEncoder.PCM16BITS),
)
More info:
NavigationRail with optional pin_leading_to_top and pin_trailing_to_bottom (#6356).ResponsiveRow for layouts whose content exceeds available height (#6417).CodeEditor.issues for displaying analysis error markers in the gutter, with analysis performed in Python (#6407).Page.pop_views_until() to pop multiple views and return a result to the destination (#6347).NavigationDrawerDestination.label now accepts custom controls; new NavigationDrawerTheme.icon_theme (#6395).DragTargetEvent.local_position and global_position (deprecating x, y, offset) (#6401).Page.theme_animation_style for customizing the theme cross-fade between theme and dark_theme (#6476).MatplotlibChart on Flutter web during animations (#6473).#c00, #fc00) rendering as invisible (#6421).auto_scroll silently doing nothing unless scroll was also explicitly set (#6404).index.html with 200 OK for missing asset files instead of a proper 404 (#6425).Lottie failing to load local asset files on Windows desktop (#6426).Page.on_resize and Page.on_media_change not firing after mobile orientation changes (#6423).flet pack desktop bundles missing the client archive on Windows and Linux (#6403).Duration fields silently decoding to 0 when given a Python float (e.g. Duration(seconds=2.0)) (#6480).page.window.destroy() taking several seconds to close Windows desktop apps when prevent_close is enabled (#6428).Page and View vertical centering when scrolling is enabled (#6450).LineChart silently dropping custom axis labels whose value matched a tick after floating-point rounding (#6459).flet_video.Video controls (#6416).See the full CHANGELOG for the complete list.
Flet 0.85.0 fills in two pieces that declarative apps were really missing: routing and dialogs. Combined with smoother video, real-time audio, and a healthy round of bug fixes, this release moves the @ft.component programming model from "promising" to "production-ready for real apps".
Try it in your apps and share feedback in GitHub Discussions or on Discord.
Happy Flet-ing!