Skip to content

State and persistence

Choose storage from the value's lifetime, owner, and purpose. Do not move a value into a global registry merely because more than one function needs it.

flowchart TD
    Value[Where should this value live?] --> Persist{Survive restart?}
    Persist -->|Yes| Preferences[SharedPreferences]
    Persist -->|No| Local{Owned by one screen?}
    Local -->|Yes| Field[Screen control field]
    Local -->|No| Route{Required to open a route?}
    Route -->|Yes| Params[Route parameters]
    Route -->|No| Runtime[AppSession only for app-wide runtime services]

BodyRegistry in ui/body_registry.py is a separate, narrowly scoped exception for sharing Home and Import/Export tile bodies with in-place search. It is not a general state store.

Learning-set content and progress do not belong in AppSession, preferences, or BodyRegistry. They live as CSV files under application storage and are accessed through data/ helpers (and AppData during a learn session). See Data and storage.

Local screen state

Keep transient UI state on the control that owns it:

class MyForm(ft.Column):
    def __init__(self):
        super().__init__()
        self.selected_kind: str | None = None
        self.title_field = ft.TextField(label="Title")

This is appropriate for form values, selection, validation messages, and other data that should disappear with the screen.

Route parameters

Use route parameters for values required to construct a particular screen:

push_view(
    page,
    SET_EDIT_ROUTE,
    file="animals_words.csv",
    title="Animals",
)

The build factory receives them as strings:

def _build_edit_set_controls(
    page: ft.Page,
    params: dict[str, str],
) -> list[ft.Control] | None:
    file_name = params.get("file")
    if not file_name:
        return None
    return [EditSetMenu(file_name)]

Route parameters make navigation reproducible and allow the factory to reject an incomplete route through its configured fallback.

Application runtime services

AppSession in ui/app_session.py owns process-wide UI objects and interaction state that should exist once while the application is running.

Shared page and file picker

AppSession.set_page(page)

page = AppSession.get_page()
export_picker = AppSession.get_export_csv_picker()
tts_service = AppSession.get_tts_service()
await AppSession.speak("insult", "en")

The export picker is created lazily and reused rather than constructing a new service for each export action. It holds the picker control only — not CSV bytes or catalog rows. Import uses a separate FilePicker on ImportExportControl. Validation and save helpers stay on CSVProcessor; see Import and export.

The export picker is created lazily and reused rather than constructing a new service for each export action. It holds the picker control only — not CSV bytes or catalog rows. Import uses a separate FilePicker on ImportExportControl. Validation and save helpers stay on CSVProcessor; see Import and export.

TTS uses one TtsService for the whole session. The Audio player is created on first speak (never with an empty src) and reused. A newer speak takes over the slash-separated playlist; the current clip is not paused or stopped. Repeating the same cached file skips remounting and the load wait so the speaker button can replay immediately. Do not add Audio controls to tiles or learn screens.

Temporary navigation lock

AppSession.disable_all_navigation_controls()
try:
    await perform_protected_operation()
finally:
    AppSession.enable_all_navigation_controls()

This coordinates the drawer, bottom-bar buttons, FAB, and home/export sort dropdowns while an operation must not be interrupted.

Do not put these values in AppSession:

  • fields belonging to one form,
  • learning-set or other domain data,
  • imported DataFrames or catalog rows,
  • values that should survive restart,
  • arbitrary controls that are not application-wide services.

Persisted preferences

Use Flet SharedPreferences via ui/preferences.py for small settings that should survive an application restart:

from learning_app.ui.preferences import get_shared_preferences

storage = get_shared_preferences()
await storage.set("theme_mode", ft.ThemeMode.DARK.value)
theme_mode = await storage.get("theme_mode")

Theme mode, background-shade, TTS, and learn-retry keys are the preference consumers today; see Theming, Text to speech, and Learning below. Keep large domain data and temporary controls out of this store.

Theming

Theme mode and content background are preference-backed and owned at runtime by AppTheme in ui/app_theme.py. The Settings screen (SettingsControl / BackgroundShadeSlider in ui/screens/settings_control.py) is the UI that edits them. The same screen also hosts Defaults / Restore defaults (resets the preference keys documented on that page and does not modify learning sets, their per-set overrides, demo install, or CSV progress), Text to speech (language, auto-speak, and word-list speakers; see Text to speech), Learning (retry until correct; see Learning), and Demo sets (install bundled samples via install_demo_sets(); that path writes CSVs and files.csv, not preferences — see Demo sets). Startup restores theme before the first route so Home does not flash the wrong look; see Startup.

Preference keys

Key Role
theme_mode "dark" or "light" (ThemeMode value)
dark_theme_slider_value / light_theme_slider_value Shade index 1–4 as a string
dark_theme_bgcolor / light_theme_bgcolor Resolved page/view background color

Missing keys are seeded once in app.py (default dark mode, slider 2, SURFACE bgcolor).

Runtime flow

  1. Startup sets page.theme_mode, applies the matching bgcolor, then AppTheme.load_from_preferences() and AppTheme.sync_from_page(page).
  2. Settings toggles light/dark: writes theme_mode, syncs AppTheme, and reapplies the bgcolor for that mode (including the saved slider shade).
  3. The shade slider updates AppTheme slider/color for the active mode, calls AppTheme.apply_to_page, and persists {mode}_theme_slider_value / {mode}_theme_bgcolor.
  4. The router and layout hosts call AppTheme.apply_to_page / current_bgcolor() when building or resizing views so stacked views stay consistent.

set_theme_from_bgcolor in ui/page_functions.py is a thin wrapper around AppTheme.apply_bgcolor used during startup.

Content background vs Shell chrome

Layer Where it lives User-editable?
Page / view bgcolor Preferences + AppTheme Yes — Settings shade slider
AppBar, bottom bar, FAB, icon/title colors Hardcoded colors map in app.py No — change the map in code

Do not put Shell teal (or other chrome colors) into preferences unless you intentionally design a second prefs surface for them.

Text to speech

TTS language, auto-speak on definitions Check, and word-list speakers have app defaults owned at runtime by TtsPreferences in ui/tts_preferences.py. Settings is the UI that edits those defaults. A set can store its own values in files.csv; an empty cell keeps using the app default. See Per-set settings. Under Text to speech, Word list speakers separates word-formation switches from definition-card switches, warns that definition text may not match the global TTS language, and shows always-visible sample cards under Word formations and Definitions. Definitions learn (WordDefinitionField) calls AppSession.speak with the set's effective language; auto-speak uses the set's effective flag and does not show error snackbars (the speaker button still does). Word-list cards (WordContainer) show per-field speakers when the set's effective switch is on.

Cached MP3s live under tts_cache/ and are pruned when set content is deleted or rewritten — see Data and storage.

Preference keys

Key Role
tts_language gTTS language code (en, pl, zh-CN, …)
tts_auto_speak_definitions "true" or "false"; pronounce the word after Check
tts_speakers_word_formations "true" or "false"; speakers on word-formation list fields
tts_speakers_word "true" or "false"; speakers on definition-list word fields
tts_speakers_definition "true" or "false"; speakers on definition-list definition fields

Missing keys are seeded in TtsPreferences.load_from_preferences() (English, auto-speak on, word-formation and word speakers on, definition speakers off).

Learning

The retype until correct switch and the two practice-queue size sliders are the app defaults, owned at runtime by LearnPreferences in ui/learn_preferences.py. Settings is the UI that edits those defaults. A set can store retry_until_correct and queue_size on its catalog row; an empty cell keeps the app default. BaseWordField reads the set's effective retry flag during Check: a wrong first answer still writes statistics once, then extra attempts stay on the same card and do not call good_answer_at_current_row / bad_answer_at_current_row. draw_index_group reads the set's effective queue size for the next group. Queue rules remain on AppData — see Learning algorithm.

Preference keys

Key Role
learn_retry_until_correct "true" or "false"; stay on a card until Check is correct
learn_word_formations_queue_size integer string; max practice-queue size for word formations (3–10, default 5)
learn_definitions_queue_size integer string; max practice-queue size for definitions (4–15, default 10)

Missing keys are seeded in LearnPreferences.load_from_preferences() (retry on, word formations 5, definitions 10). A queue size that cannot be read as an integer becomes that default. A stored integer outside the range is clamped to the minimum or maximum.

Where BodyRegistry fits

BodyRegistry in ui/body_registry.py stores only the active Home and Import/Export TilesContainer instances so in-place search can filter the same controls the user was already viewing. It should not hold preferences, route parameters, form state, or general domain data. See Body registry.

Where set data fits

Catalog rows, set CSVs, and learning statistics belong on disk via data/file_path_manager.py and data/app_data.py. Per-set learning and speech overrides are cells on the catalog row (data/set_settings.py), not preference keys. Resetting progress uses set_default_progress; deleting a set uses delate_set. During a learn session, queue and answer transitions live on AppData — not in AppSession. Retry and queue size for that session are the set's effective values. See Data and storage and Learning algorithm.

For generated contracts, see the App session API, Preferences API, App theme API, TTS preferences API, TTS API, Learn preferences API, Set settings API, Body registry API, File path manager API, App data API, Constants API, and CSV processor API.