Project Structure
Understanding how Whitehall projects are organized.
File Layout
myapp/
├── whitehall.toml # Project config
├── theme.css # Optional: the theme, as CSS custom properties
├── .env # Environment vars (base, PUBLIC_* only)
├── .env.local # Local overrides (gitignored)
├── .env.debug # Debug overrides
├── .env.release # Release overrides
├── src/
│ ├── main.wh # Optional: the shell around the router
│ ├── routes/ # File-based routing
│ │ ├── +screen.wh # → /
│ │ ├── +layout.wh # Root layout
│ │ ├── +error.wh # What renders when a loader below it throws
│ │ ├── +screen.load.wh # Screen loader
│ │ ├── (auth)/ # Route group: shares a layout, adds nothing to the path
│ │ │ └── +layout.wh
│ │ └── user/[id]/
│ │ ├── +screen.wh # → /user/:id
│ │ ├── +screen.load.wh
│ │ └── +screen.rs # The screen's logic, in Rust
│ ├── lib/ # Everything else: components, stores, models —
│ │ │ # organized however the project likes
│ │ ├── components/
│ │ ├── stores/
│ │ └── models/
│ ├── widgets/ # Home screen widgets
│ │ └── +Clock.wh # Widget (+ prefix)
│ ├── notifications/ # Notification channels
│ │ └── +Alerts@high.wh # Channel with priority
│ ├── receivers/ # Broadcast receivers
│ │ └── +Boot.wh
│ └── android/ # Your own Kotlin, if any
├── i18n/
│ └── en.toml # Translated strings — see $t
├── static/
│ ├── icon.png # App icon
│ └── fixtures/ # $import files
├── .whitehall/ # Framework workspace (generated Kotlin & Gradle project)
└── dist/ # Final deliverables (app-debug.apk, app-release.apk) The Shell — src/main.wh
Optional. Its body wraps every screen, with the router mounted where <slot /> is — the same move +layout.svelte makes in SvelteKit, one level up:
<App theme={Settings.mode}>
<Column class="w-full h-full">
<OfflineBanner />
<slot />
</Column>
</App> Absent, it is <App><slot /></App>: the router is the whole app.
File Naming Conventions
Screens
+screen.wh- Screen component+layout.wh- Layout wrapper+screen.load.wh- Screen data loader+layout.load.wh- Layout data loader+error.wh- What renders when a loader below it throws+screen@.wh- Screen with no layouts+screen@root.wh- Screen with only root layout+layout@.wh- A layout that starts a new chain+screen.rs,.c,.cc,.cpp,.zig- The screen's logic in another language(name)/- A route group: shares a layout, adds nothing to the pathmoney.test.wh- A test. No+, because+means it ships
See Layouts for nesting, groups and the @ forms.
Dynamic Routes
Use [param] folders for dynamic segments:
routes/
├── user/[id]/+screen.wh # → /user/:id
└── posts/[slug]/+screen.wh # → /posts/:slug Access params via $screen.params.id.
Widgets
Widget files must start with + and live in src/widgets/:
src/widgets/
└── +Clock.wh # Home screen widget Notifications
Notification channel files use +Name@priority.wh format:
src/notifications/
├── +Alerts@high.wh # High priority
├── +Messages@default.wh
└── +Updates@low.wh Two directories mean something to the compiler: src/routes/ (a file there is a
screen) and src/lib/ (imported as $lib.…). widgets/, notifications/, receivers/ and wear/ are platform
entry points. Any other folder is a package name and nothing more.
Package Mapping
Any directory under src/ maps to a package suffix of the same name — src/lib/ is not special-cased, it is just a directory whose sub-folders are
the project's own choice.
| Path | Package |
|---|---|
src/lib/components/Foo.wh | app.lib.components.Foo |
src/routes/user/[id]/+screen.wh | app.routes.user.id.UserScreen |
src/lib/stores/Auth.wh | app.lib.stores.Auth |
src/lib/models/User.wh | app.lib.models.User |
src/widgets/+Clock.wh | app.widgets.Clock |
Import Aliases
Whitehall provides convenient import aliases:
| Alias | Expands To |
|---|---|
$lib.models.X | com.example.app.lib.models.X |
$lib.stores.X | com.example.app.lib.stores.X |
$lib.components.X | com.example.app.lib.components.X |
$native | this screen's sibling native module (+screen.rs, +screen.zig, …) |
$native.codec | src/lib/codec.rs |
$tool | the program this directory was before init --app |
$icons.lucide.Heart | one icon out of the lucide set |
$wear.stores.X | src/wear/stores/X.wh |
One import per line. $app is reserved for the framework and names nothing
today — a literal route string is the one spelling for navigating, so there is no
generated routes object to import.
Configuration
The whitehall.toml file configures your project:
name = "my-app"
version = "0.1.0"
package = "com.example.myapp"
[screen]
routes = "src/routes"
[android]
min_sdk = 24
target_sdk = 36
[toolchain]
java = "21"
gradle = "8.4"
agp = "8.2.0"
kotlin = "2.0.0"
[dependencies]
"io.coil-kt/coil-compose" = "2.6.0"
[android.icons]
default = "static/icon-default.png"
dark = "static/icon-dark.png" See Configuration for the full shape.