Quiblo wiki

Architecture

Overview

Layers, the dependency rule, and the build-time check that makes a second frontend cheap.

# The shape

A conventional layered Android architecture, with one rule enforced harder than usual.

:app :app-tv :feature:* :source:* :core:media :core:model · :core:data · :core:database :core:datastore · :core:network · :core:common phone · dev.quiblo.player television · dev.quiblo.tv browse, live, vod, series, player, sources, settings, favorites m3u · xtream · iptvorg · api the player engine, behind an interface
Dependencies point downwards only. Nothing below :feature:* may import Compose — a rule the build enforces, and the reason a second frontend cost a presentation layer rather than a rewrite.

An application depends on features; features depend on core and source; core depends only on core. Nothing points back up, and nothing skips sideways between features — a feature that needs something from another feature is a sign that the something belongs in :core:*.

# The rule that pays for itself

No UI code in :core:* or :source:*. No Compose import, no Android Context beyond what Room and DataStore require.

This is checked by the build — a convention plugin calls enforceNoCompose(), and no core or source build file may reference Compose or a feature module. It is not a code review convention that erodes; it fails the build.

The payoff was measured rather than assumed. When the television frontend was proposed, the question "how much of this ports?" had a concrete answer:

LayerTelevision cost
Model, database, DataStore, network, media, dataZero. Used unchanged
M3U and Xtream parsersZero. Used unchanged
ViewModelsZero. They hold no Compose types
ScreensAll of it. Every composable is touch-shaped and none ports

So a second application was a presentation layer, not a second product. The unused phone screens come along as compiled code but are unreferenced from the television's graph, and R8 strips them — confirmed rather than assumed: the television APK is smaller than the phone's despite depending on every feature module.

# One ViewModel, two frontends

The television reuses the phone's ViewModels directly. BrowseViewModel, PlayerViewModel, SeriesDetailViewModel, MovieDetailViewModel, SourcesViewModel and SettingsViewModel each serve both apps.

Do not fork them. The moment there are two BrowseViewModels, a fix to the guide-request guards has to be made twice — and the second one will be forgotten, which is how the provider block comes back.

The same reasoning applies below the ViewModels. When the television needed a text field that survives the on-screen keyboard, the fix went into a shared component rather than being copied into the two screens that need typing: a copy each is how a fix lands in one and is forgotten in the other.

What is shared when the drawing cannot be

Sometimes both apps need the same answer and cannot possibly draw it the same way. The programme timeline is the clearest case: a phone drags it under a finger and a television walks it with a D-pad, but where each programme sits, how wide it is and which one is on now are the same arithmetic on both.

So the arithmetic is a plain function with no Compose in it — guideTimeline in :feature:browse — and each app draws its own strip from the fractions it returns. The decisions are shared; the drawing is not. That split is also what makes the awkward parts testable: overlapping listings, holes a provider left, and programmes that began before the window are all decided in a JVM test rather than argued about in front of a television.

# Dependency injection and state

Koin for injection — each module contributes a Koin module, and an application assembles the list it needs. The television's list is the phone's list; it differs only in which screens consume it.

Kotlin Flow throughout, with StateFlow at the ViewModel boundary. A screen collects one immutable state object rather than several independent streams, so it cannot render a half-updated combination.

Room for storage, DataStore for preferences and credentials, OkHttp for network, Media3 for playback, Coil for images.