Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
app-common
Android modules shared between Bryan's apps, each one directory of this Gradle build.
| Module | What it is |
|---|---|
controls/ |
Icon glyphs and the buttons around them at one set of sizes, the text field with its label above (LabelledField) or without one (UnlabelledField, taking a screen-reader description instead), the setting rows and pickers, tabs between ways of editing one thing's settings (SettingTabs), a loading spinner for a list still loading (LoadingSpinner), fading text, and which way lists run (ListDirection, with order and arrangement for laying a list out that way). Every app draws these alike; the icon font is the app's own, through LocalGlyphFont. |
theme/ |
The colour scheme (AppTheme): the system's, deepened in the dark, with Catppuccin's accents for colours that mean something; the light/dark setting (Theme, ShownTheme); system bars that follow it; and ActionTone. |
bars/ |
UnderBars: content scrolling under blurred bars at the top and bottom, with the screen's header in whichever the header setting names, the tab row for one, a footer slot for rows such as a selection's actions (always in the bottom bar, above the rest), a strip slot for notices that stay in view (in the header's bar, on the content's side), pull to refresh, and keeping a list at its end. |
file-browser/ |
A browser over any source of directories and files, a file viewer, and the syntax highlighter. See its DESIGN.md. |
markdown/ |
Markdown drawn as a comment or a document: links, lists, tables, highlighted fences, images the app loads. Uses file-browser's highlighter. See its DESIGN.md. |
graphs/ |
Graph widgets drawn from plain numbers, labels and colours: LineChart (gaps, dashed reference lines, cheap at any length), horizontal BarChart with floors and counts, CategoryStrip of events over x, Heatmap, and StatReadout and TextReadout. Styled from theme's Catppuccin accents (graphColors); it knows nothing of where the numbers came from. |
rust/ |
The Rust crate app-common, the same design layer for apps on iris: theme (the colour scheme as AppTheme builds it, from iris's SystemTheme; Catppuccin's accents; ActionTone; the light/dark setting, Theme) and typography (APP_TYPOGRAPHY, and WithTextStyle to set a text to one of its roles). Plain data; an app depends on it by git. |
Using a module
An app takes this repository as a git submodule and includes only the modules
it uses, as subprojects pointed at their directories -- so it is pinned to the
exact commit its submodule names, and nothing is published. In the app's
settings.gradle.kts, with the path relative to that build:
include(":file-browser")
project(":file-browser").projectDir = file("../app-common/file-browser")
and implementation(project(":file-browser")) in the module that uses it.
A subproject takes its plugins and libraries from the build including it,
so every accessor a module's build script names must be in the app's version
catalog under the same name. file-browser uses controls and bars, so an
app including it includes all three; between them they name:
- plugins
androidLibrary,composeCompiler,ktfmt - libraries
compose-runtime,compose-foundation,compose-ui,compose-material3,androidx-activity-compose,androidx-core-ktx - for its tests,
kotlin-test-junit5,junit-jupiter,junit-platform-launcher(JUnit 6, run on the Platform) hazeandhaze-blur, forbars
markdown uses file-browser, so an app including it includes all four, and
names markdown-renderer besides.
A FileBrowserState is closed by the app that made it. Its scope outlives
the screen, so an open indexed file would go on fetching after the browser is
gone. Call state.close() in the same composition scope that remembers the
state, right beside it, so it runs exactly when the remembered state is
dropped:
val state = remember(source, target) { FileBrowserState(…) }
DisposableEffect(state) { onDispose { state.close() } }
A host that keeps the state longer -- in a ViewModel, or a
rememberSaveable-backed holder -- calls close() where that is cleared.
theme and bars each use controls. theme needs nothing beyond the list
above but the two haze entries, which only bars names. graphs uses
theme, so an app including it includes theme and controls too, and it
likewise names nothing beyond the list but the haze entries.
Settings the app keeps
Four settings are drawn by these modules but stored by the app: the theme
(Theme, in theme), which way lists run, which way settings lists run, and
which end of the screen headers sit at (each a ListDirection, in controls,
each its own toggle). Each has a holder of the value being drawn --
ShownTheme, ShownListDirection, ShownSettingsDirection and
ShownHeaderDirection. The app calls restore(context)
before its first frame, which shows the copy this device last drew, then
hands every value it learns, from a server or its own storage, to show.
That copy is only for the first frame; where the setting really lives is the
app's. The app provides three locals around everything it draws:
ShownListDirection.direction through LocalListDirection, which the bars'
pull to refresh and KeepAtEnd follow; ShownSettingsDirection.direction
through LocalSettingsDirection, which SettingsList follows; and
ShownHeaderDirection.direction through LocalHeaderDirection, which decides
whether UnderBars' header goes in its bottom bar (FromBottom) or its top
one (TopDown). A screen passes its title row as header, and keeps top and
bottom for what belongs at that edge either way.
AppearanceRows (in theme) draws an Appearance section's shared rows: the
theme picker, then the three direction toggles. Each row shows its holder's
value and hands a change to its hook -- onTheme, onHeaders, onLists and
onSettings -- which defaults to that holder's show. An app that keeps a
setting elsewhere, such as on its server, passes what stores it there, and that
must end by calling the holder's show. The app can add its own rows in the
same section.
SettingsList scrolls itself, from the end it reads from, so a screen must not
wrap it in a verticalScroll of its own; it takes the bars' listPadding as
contentPadding. Everything on a settings screen goes in a section, form fields
included, so it mirrors with the rest. Each direct child of a section is one
row, with anything belonging to it (its error, its buttons) grouped in that
child, and the section spaces the rows, so a screen draws no spacers.
A choice is a PickerRow (or a bare PickerButton, or GlyphPickerButton).
Each lists its options by text, and the button shows the current one by
buttonText, which defaults to text and is there for a shorter form -- a
commit's short hash on the button, hash and subject in the menu. An option
enabled answers false for stays listed, drawn disabled, and cannot be picked;
the button is pressable only while some enabled option other than the current
one exists. A disabled SwitchRow dims its label and glyph as the switch dims.
ListDirection.order(items) is a list's items top to bottom as that direction
draws them -- as given from the bottom, mirrored top-down -- and is its own
inverse, so an order rearranged as drawn maps back through it.
ListDirection.arrangement(gap) spaces rows gap apart and holds a short list
to the end the direction starts from.
bars draws the page behind a screen with the scheme's background unless the
app provides LocalPageBackground, a function from the ColorScheme in force
to the page's Modifier, and LocalOverPage is applied to what is
drawn over that page.
gradle/libs.versions.toml here is the reference for all of them, and is what
this build tests with.
Building here
This build exists so each module can be built and tested on its own:
ANDROID_HOME=~/Android/Sdk ./gradlew :file-browser:testDebugUnitTest :markdown:testDebugUnitTest :file-browser:lintDebug :markdown:lintDebug :controls:lintDebug :theme:lintDebug :bars:lintDebug :graphs:testDebugUnitTest :graphs:lintDebug
ANDROID_HOME=~/Android/Sdk ./gradlew ktfmtFormat ktfmtFormatScripts :controls:ktfmtFormat :controls:ktfmtFormatScripts :file-browser:ktfmtFormat :file-browser:ktfmtFormatScripts :markdown:ktfmtFormat :markdown:ktfmtFormatScripts :theme:ktfmtFormat :theme:ktfmtFormatScripts :bars:ktfmtFormat :bars:ktfmtFormatScripts :graphs:ktfmtFormat :graphs:ktfmtFormatScripts
Tests and lint pass, and the formatter changes nothing, before anything is pushed.