Skip to content

Adding a screen

Use ui/screens/_screen_template.py as the implementation starting point. Before copying it, decide what role the new screen has.

Decide: Shell or Deep?

  • Choose Shell when the screen is a main destination, can replace the current navigation stack, and may appear in the drawer.
  • Choose Deep when the screen is a focused task opened from another view and closed with Back, Cancel, or Save.

See Two navigation roles for the reason behind this distinction.

The remaining work is usually: define a path, implement the control, create a factory, configure Shell chrome if needed, register a RouteDef, and navigate through the public helpers.

1. Define the path

Add a constant to ui/route_paths.py:

MY_SCREEN_ROUTE = "/my-screen"

A named constant keeps the path in one place and prevents slightly different route strings from being repeated in controls, registry entries, and tests. See the route paths API.

2. Create the screen control

Place the control in ui/screens/. Keeping UI construction in a screen class separates screen behavior from route lookup and view-stack management.

Choose the simplest variant that matches the screen:

Use a regular Flet control when the wrapper can handle sizing and the screen has no nested controls that require custom resize logic.

class MyScreen(ft.Container):
    def __init__(self, page: ft.Page):
        super().__init__(expand=True)
        self.content = ft.Text("My screen")

Add RoutableScreenMixin and apply_layout() when the screen must resize itself or nested controls from the current LayoutMetrics.

from learning_app.ui.layout_metrics import LayoutMetrics
from learning_app.ui.routable_screen import RoutableScreenMixin


class MyScreen(RoutableScreenMixin, ft.Container):
    def __init__(self, page: ft.Page):
        super().__init__(expand=True)
        self._app_page = page
        self.content = ft.Text("My screen")

    def apply_layout(
        self,
        metrics: LayoutMetrics | None = None,
    ) -> None:
        metrics = self.resolve_layout_metrics(metrics)
        self.width = metrics.body_width
        self.update_if_mounted()

See the Layout guide for the resize lifecycle and the routable screen API for the mixin contract.

3. Add a build factory

Build factories in ui/route_registry.py receive the page and decoded query parameters. The factory isolates how the route creates its screen, so the router does not need screen-specific constructors.

def _build_my_screen_controls(
    page: ft.Page,
    _params: dict[str, str],
) -> list[ft.Control]:
    from learning_app.ui.screens.my_screen import MyScreen

    return [MyScreen(page)]

Return raw controls. The router applies the configured body wrapper after the factory returns. Local imports help avoid circular dependencies between screens, navigation, and route registration.

A factory may return None when required route data is missing. In that case the router builds the configured fallback route.

4. Configure Shell chrome

For a Shell route, add its ShellChromeConfig to SHELL_CHROME in ui/chrome_config.py before registering the route. Existing Shell RouteDef entries index that map (chrome=SHELL_CHROME[…]), and the router also looks up chrome by path in SHELL_CHROME at runtime:

MY_SCREEN_ROUTE: ShellChromeConfig(
    appbar_title="My screen",
    appbar_menu_leading=True,
    bottom_appbar_visible=False,
    fab_visible=False,
    search_button_visible=False,
),

This keeps shared AppBar, bottom bar, FAB, and search visibility out of the screen's business logic. Deep routes need no entry here. The live controls themselves are registered once on AppChrome at startup — see Chrome and wrappers. APIs: chrome configuration, App chrome.

5. Register the route

Add a RouteDef to ROUTE_REGISTRY in ui/route_registry.py. This is the single place that connects a path with its construction, navigation role, wrapper, layout behavior, and optional drawer metadata.

RouteDef(
    path=MY_SCREEN_ROUTE,
    kind=RouteKind.SHELL,
    body_wrapper=BodyWrapperKind.BOTTOM_INSET_SHELL,
    layout_kind=LayoutKind.SHELL,
    chrome=SHELL_CHROME[MY_SCREEN_ROUTE],
    build_factory=_build_my_screen_controls,
    drawer_label="My screen",
    drawer_icon="STAR",
)

Required fields

  • path — the constant defined in ui/route_paths.py.
  • kind — RouteKind.SHELL or RouteKind.DEEP.
  • body_wrapper — the outer body container selected by the router.
  • layout_kind — the resize-dispatch strategy.
  • chrome — pass SHELL_CHROME[path] for Shell (after step 4), or None for Deep. Stored on the registry entry for consistency with existing routes; runtime AppBar / bottom bar / FAB / search visibility comes from SHELL_CHROME[path] in the router, not from reading route_def.chrome.
  • build_factory — the function that returns raw controls or None.

Optional fields

  • fallback_path — opened when the factory returns None.
  • shell_layout_control — control type the Shell dispatcher should find when applying custom layout.
  • body_content_alignment — vertical alignment inside a Deep body column.
  • vertical_alignment — alignment applied to the complete ft.View.
  • drawer_label and drawer_icon — include a Shell route in the drawer.
  • drawer_divider_before — place a divider before that drawer item.

The registration above matches the simple screen variant and intentionally omits shell_layout_control. For the responsive variant, add:

shell_layout_control=MyScreen

The mixin applies initial layout after mounting. The router uses this type to find the control and pass fresh metrics after resizing and restoring the view.

See the route registry API for complete field types and defaults.

Choose LayoutKind

Value Use when Existing example
HOME The route owns the Home tile body and its flex-layout refresh. Home
IMPORT_EXPORT The route hosts the Import/Export control and tile body. Import/Export
SHELL A normal Shell route may contain a routable screen control. Settings, Info
DEEP_FORM A Deep route contains a responsive form or learning flow. Create, Edit, Learn

The first two values are specialized for existing application flows. A new ordinary Shell screen normally uses SHELL; a new Deep form normally uses DEEP_FORM.

Choose body_wrapper separately. It controls the outer container, padding, and safe area rather than resize dispatch. See Chrome and wrappers.

6. Navigate and verify

Use navigate_to() for Shell and push_view() for Deep. These helpers express the intended stack behavior without exposing router internals. See the Navigation guide and navigation API.

Before considering the route complete, verify:

  • The screen opens through its intended action.
  • Back, Cancel, or Save reveals the expected previous screen. On an Android APK, also press the system Back button: the previous Shell must still show its AppBar and bottom bar. Browser Back and flet run --android do not cover that path.
  • Missing required route parameters use the expected fallback.
  • The layout updates after resizing.
  • AppBar, bottom bar, FAB, search, and safe-area behavior are correct.
  • Drawer label, icon, selection, and divider are correct when configured.

Existing routes and owning classes are listed in Routing and screens.