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
TabbedPanelor a custom tab strip where switching is not "deeper" - useswitch_tobut 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
Popupwidget (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.