Skillversitas

Screens and Navigation

A real app is more than one screen: a list, a detail view, an add form, a settings page. Kivy's ScreenManager and Screen are the navigation layer - a manager that holds named screens and swaps the visible one with a transition, and the switch that is the app's notion of "which page am I on." This chapter builds the recipe manager's three screens (list, detail, add) on a ScreenManager, covers the switch_to navigation, passing data between screens, and the back-button behavior that a multi-screen app needs.

ScreenManager and Screen

from kivy.app import App
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.lang import Builder

Builder.load_string("""
ScreenManager:
    id: manager
    ListScreen:
        name: "list"
    DetailScreen:
        name: "detail"
    AddScreen:
        name: "add"

<ListScreen>:
    BoxLayout:
        orientation: "vertical"
        Label:
            text: "Recipes"
            size_hint_y: None
            height: 50
        Button:
            text: "New recipe"
            size_hint_y: None
            height: 50
            on_press: root.manager.switch_to("add")
        Label:
            text: "Tomato Soup\\nChili\\nRamen"

<DetailScreen>:
    BoxLayout:
        orientation: "vertical"
        Label:
            text: "Recipe detail"
        Button:
            text: "Back"
            size_hint_y: None
            height: 50
            on_press: root.manager.back()

<AddScreen>:
    BoxLayout:
        orientation: "vertical"
        Label:
            text: "Add a recipe"
        Button:
            text: "Done"
            size_hint_y: None
            height: 50
            on_press: root.manager.switch_to("list")
""")

class RecipeApp(App):
    def build(self):
        # the first rule loaded above IS the ScreenManager
        return Builder.get_root()

if __name__ == "__main__":
    RecipeApp().run()

The structure: a ScreenManager is the root, and its children are Screen widgets each with a unique name. The manager shows one screen at a time, and manager.switch_to("detail") swaps to the named screen, playing the transition (the animation chapter's SlideTransition by default). manager.back() goes to the previous screen in the history - the back-button behavior for free, because the manager keeps the visit order. The on_press: root.manager.switch_to(...) lines are the navigation wiring - a button on one screen calls a method on the manager to change the page. The three screens of the recipe manager (list with a "New" button, detail with a "Back" button, add with a "Done" button) are the minimal multi-screen app, and every navigation is a switch_to or a back().

Passing Data Between Screens

from kivy.properties import StringProperty, NumericProperty

class DetailScreen(Screen):
    recipe_name = StringProperty("")
    servings = NumericProperty(0)

    def load(self, name, servings):
        # the list screen calls this BEFORE switching, so the
        # detail is ready when it appears
        self.recipe_name = name
        self.servings = servings

class ListScreen(Screen):
    def open_detail(self, name, servings):
        detail = self.manager.get_screen("detail")
        detail.load(name, servings)        # populate first
        self.manager.switch_to("detail")    # then navigate

Screens are widgets, so they hold properties, and the navigation pattern is populate-then-switch: the source screen reaches the target via manager.get_screen("detail"), sets its properties (or calls a load() method), and only then calls switch_to. The detail screen's KV binds its labels to its own properties (text: root.recipe_name), so the data set before the switch is visible the moment the screen appears. The alternative - a shared state object on the App that both screens read - is the better shape when the data is large or shared by more than two screens: the list writes the selected recipe to app.current_recipe, the detail reads it on entry. The rule is the same as the properties chapter: one direction of data flow into the screen being entered, set before the transition, so the screen never renders an empty state for a frame.

History, Back, and the App Lifecycle

class RecipeApp(App):
    def build(self):
        return self.root

    # react to the system back button (Android) / escape (desktop)
    def on_back_button(self):
        mgr = self.root
        if mgr.current != mgr.get_screen("list"):
            mgr.back()
            return True       # handled - stop the default (quit)
        return False          # on the home screen: let it quit

    # a "can go back" indicator for the UI
    def can_go_back(self):
        return self.root.current != self.root.get_screen("list")

The ScreenManager keeps a history, and back() walks it - but the platform back gesture (the Android hardware back, the desktop escape) is a separate event the App handles. The pattern: on the back event, if there is a previous screen, mgr.back() and consume it; if the app is already on its home screen, return False so the default behavior (quit) happens. This two-level rule - "back goes to the previous screen until you are home, then it exits" - is the expected navigation on mobile, and wiring it is the difference between an app that feels native and one that quits on the first back press. The can_go_back helper drives the UI affordance (a back arrow that only shows when there is somewhere to go), which is the polish that makes the navigation readable.

Navigation Patterns

  • Stack navigation (list -> detail -> sub-detail): the default switch_to/back() history is exactly this; each level is a screen, back pops it.
  • Tab navigation (peers, not a stack): a TabbedPanel or a custom tab strip where switching is not "deeper" - use switch_to but reset the history (manager.available_hashes / clear) so back does not walk the tabs. The tabs are a peer choice, not a stack, and the navigation model should match.
  • Modal over a screen: a dialog or an overlay shown over the current screen rather than a new screen - the Popup widget (a screen-sized overlay) is the tool, and it does not touch the manager's history at all.
  • Deep state: when the "current screen" is a function of data (which recipe's detail), keep the data as the source of truth and let the screen be a view over it - navigating is changing the data, and the screen follows.

Summary

Build the multi-screen app on a ScreenManager root whose children are named Screen widgets; navigate with manager.switch_to("name") (forward) and manager.back() (pop the history), with the transition from the animation chapter playing on each swap. Pass data populate-then-switch - set the target screen's properties (or a shared app state) before switch_to, so it renders complete. Handle the platform back gesture at the App level: back() until home, then let the default quit happen, and drive a back-affordance from a can_go_back check. The four patterns - stack (the default history), tabs (peers, reset the history), modal (Popup over the current screen), and data-driven (the screen is a view over shared state) - cover the navigation shapes a real app needs, and the recipe manager's three screens are the stack pattern made concrete.