Code reference · Application
:app-tv
The television assembly and all of its UI.
dev.quiblo.tv
There are no :feature-tv:* modules: there is one consumer, and a module per
screen would be structure without benefit.
Navigation is a hand-rolled sealed overlay state rather than Navigation-Compose. That is deliberate — the tab bar's focus model was hard-won, and a navigation library's own focus restoration is the most likely thing to undo it.
sealed interfaceTvOverlay
What is drawn over the shell: playing, a film, a series, or settings.
One piece of state rather than four booleans, because the states are exclusive. Four flags can express "playing and in settings", which is not a thing, and the code would have to keep proving it never happens.
sealed interfaceTvPlaybackRequest
What the player was asked to play: a live channel, a film, or an episode.
The three cases exist because they are genuinely three things, and flattening them into "a Channel and a list" was a reported bug. A film got the channel keys mapped to "zap to the next film", the controls announced it as live, and a series — whose row carries no episode stream — was asked to play a URL that plays nothing.
Making the distinction a type means the player cannot be handed a series by accident and cannot forget to pass an episode's URL: there is no shape of this class that expresses either mistake.
Live | Carries the queue it was chosen from, because zapping needs it and the player cannot know it. |
|---|---|
Film | Carries a nullable start position — null resumes, zero restarts. |
Episode | Carries the stream URL and the numbering, neither of which is derivable from the series row, plus the whole run of the series and where this episode sits in it. |
zappedBy(direction) | The channel that many steps away, wrapping. Live only. |
steppedBy(direction) | The neighbouring episode, or null at either end. It does not wrap — a channel list is a ring, a series is a thing that finishes. |
hasNext / hasPrevious | Whether there is an episode either side, so the player draws a button only where one leads somewhere. |
classTvPlayerControls
The player as focusable buttons: play in the middle, seeks either side, episode steps outside those, and options underneath.
It replaced a readout. The remote's own keys still drive playback with nothing on screen — that is the fastest way to pause something — but a remote has keys for about five things, and every feature past those five was arriving on a key that already meant something else.
Each button carries its own four directions. Compose finds a focus target by looking at where things are, and the two rows barely line up: left to geometry, down from the play button landed on the options row's last button, and right off the end of that row jumped back up to the transport. Measured with a D-pad walk at the panel's real geometry, not argued.
Nothing changes size on focus. A focused control that grows reports a rectangle that moves every frame while the animation runs, and that has already cost this project four wrong answers about a shaking catalogue.
TvControlsState | Everything the controls draw from — one value rather than a dozen parameters that stopped saying which is which. |
|---|---|
TvControlActions | What the buttons do. The screen supplies them; this file only decides where they sit. |
classTvNextEpisodeBanner
The end of an episode, and the offer of the next one on a countdown.
It slides in from the right because it arrives on its own: something appearing unasked has to be seen arriving, or a viewer looks up at a countdown already at two with no idea what started it. Focus lands on Play now, so carrying on is one press.
The closest thing this app puts on a television to a modal, and deliberately not one — playback has already stopped so nothing is covered, it takes a corner rather than the middle, it cannot be arrived at by accident, and both ways out are on screen at once.
shouldOfferNextEpisode(request, status, isDismissed) | When the offer appears. A plain function, because every mistake it can make is silent — offering one for a film, at the end of a series, again after a refusal, or after a failure rather than an ending. |
|---|
sealed interfaceTvBarState / TvBarAction / tvBarAction
Where the remote is along the top bar, and what a key press does to it.
Extracted as a plain function so the one thing a reviewer needs to check is not buried in a modifier, and so the awkward cases can be asserted rather than tried by hand on a television.
The settings gear is a position along the bar rather than a focusable of its own. An icon that opts out of the bar's one-focusable model cannot be reached at all: it sits inside that focusable, so a focus search walks past it into the content below. The gear was unreachable by remote for the entire life of the app.
data classKeyActions / handleKey
The player's remote vocabulary, in one testable place.
On a television the key map is the interface. Where a key does nothing — seeking on a live stream, the channel keys during a film — it is left unhandled rather than swallowed, so it falls through to the system instead of being absorbed by a player that has nothing to do with it.
enumTvTab
Search, Live, Movies, Series, Favourites.
Search is first and is the only one drawn as an icon: a magnifier needs no word beside it, and the leftmost position is where a remote already rests when the app opens. It is also where Back comes to rest — Back from any catalogue lands here, and Back again leaves the app, so a viewer pressing it repeatedly visibly gets closer to leaving rather than cycling.
Sources is deliberately not here. It lives in Settings: adding a playlist is something a viewer does once, and every position on the bar is one more press between somebody and what they came to watch.
classTvSearchScreen
The search tab, with a resting shape and a working one.
At rest it is the name and a field in the middle of the panel and nothing else — the whole screen says "type something", which is the only thing to do there. Asking a question, or opening Advanced, moves the field to the top and gives the rest of the panel to the answer.
It reuses TvCategoryList whole rather than laying out rows of its own. That is
not only economy: that list is the composable measured by the scroll-stability test, and the
modifier order inside its poster is the fix for the shake.
A second row implementation here would be a second place for it to come back unmeasured.
classTvProfileScreen / ProfileGate
The chooser, standing in front of the app until somebody has said who they are.
Not one of the overlays. Nothing is drawn that would have to read a favourite or a resume point before the app knows whose they are — which means no screen below has a state for "no profile" at all.
ProfileGate is the phone's equivalent, doing the same job for the same
reason.
data classTvCategoryRow / TvRowItem
A category's posters, each carrying its position in the flat list.
The index travels with the item rather than being recovered. Finding it with
indexOf meant a linear scan of the whole catalogue on every press — unnoticeable
on a short list, and a visible pause on a large one. Grouping records the index in the same
pass, so it costs nothing to know.
classQuibloTvApplication / TvMainActivity
Koin startup, and the single landscape-locked activity.