Module Cafeyn Group SDK

Bring press content into an Android app: browse publications, issues, articles and videos, download issues for offline reading, and open them in a full reader — with or without the SDK's own screens.

Pick a module from the list at the bottom of this page to browse its API.

Modules

Module Artifact What it gives you
core co.cafeyn:cgs-core Content catalogue, authentication, downloads, and the reader, opened as an Activity. No screens of its own.
ui co.cafeyn:cgs-ui Ready-made Jetpack Compose screens, the blocks to compose your own, and the design system that themes them.

ui depends on core — depending on ui pulls the whole SDK in. Take core alone to build your own interface on top of the catalogue and the reader. The reading engine itself ships as co.cafeyn:cgs-reader, a dependency of core. The one part of it your code touches is its theme — OneReaderTheme and the OneReaderColors palette it carries, which every Reader.open overload takes to restyle the reader. Nothing else in it is meant to be called, and none of it is documented here.

Requirements

Kotlin 2.3 or later. Every point in time on the API is a kotlin.time.Instant — IssueShort.releaseDate, Media.creationDate, DateRange.from, CgsStreamState.Success.succeededAt and the rest — and that type became Stable in Kotlin 2.3.

There is nothing to opt into and no compiler flag to set; reading a date is an ordinary property read. Still on java.util.Date elsewhere in your app? Date(issue.releaseDate.toEpochMilliseconds()) converts a catalogue date exactly.

Catalogue dates arrive without a time zone and are read in the device's: format them in the device's time zone — what date formatters use by default — not in UTC.

Getting started

0. Add the dependency

The SDK is published to its own Maven repository, which needs no credentials. Declare it in settings.gradle.kts, next to the repositories you already use:

dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven {
url = uri("https://maven-android-sdk.milibris.net/")
content { includeGroup("co.cafeyn") }
}
}
}

Then depend on the module you need, at the version shown at the top of this page:

dependencies {
implementation("co.cafeyn:cgs-ui:VERSION") // the screens, blocks and design system; brings in core
// or
implementation("co.cafeyn:cgs-core:VERSION") // the catalogue, authentication, downloads and reader only
}

co.cafeyn:cgs-reader comes in through core. Don't declare it yourself.

1. Declare your license

<application>
<meta-data
android:name="co.cafeyn.cgs.license"
android:value="YOUR_LICENSE_KEY" />
</application>

You can pass the key to SdkConfig.Builder.apiKey instead if you would rather keep it out of the manifest.

2. Initialize once, at startup

class MyApp : Application() {
override fun onCreate() {
super.onCreate()
CGS.init(
SdkConfig.Builder(this)
.environment(CgsEnvironment.PRODUCTION)
.productName("YOUR_PRODUCT_NAME")
.onSessionLost { /* re-authenticate */ }
.build()
)
}
}

Calling CGS.auth(), content(), download() or reader() before this throws InitializationException.

If your app has no notion of a user profile, say so here and you never have to think about profiles again:

SdkConfig.Builder(this)
.productName("YOUR_PRODUCT_NAME")
.selectsProfileAutomatically(true)
.build()

The SDK then adopts the account's first profile as soon as authentication returns it.

3. Authenticate

CGS.auth().authenticate(hostToken = yourBackendToken)

That is the whole of it when the SDK selects the profile: content is ready to read, scoped to the profile it adopted, or to the account if it holds none.

If your app does manage profiles, choose one before showing content. The profiles are yours — the SDK does not hand back a list of its own, so pass the id you already know from your backend:

CGS.auth().selectProfile(ProfileId(yourProfileId))

Nothing is active until you do, and that is a supported state rather than a broken one: the session simply reads at account scope. Call refreshProfiles(hostToken) when you know your profiles have changed, so the SDK notices a profile that is gone.

CgsAuth.state tells you where the session stands, and onSessionLost fires if it is ever dropped for good.

4. Read the catalogue

val publications = CGS.content().publications.all(scope = viewModelScope)
viewModelScope.launch { publications.state.collect { state -> render(state.items) } }

// From the list's scroll listener, as the user nears the end:
fun onNearEnd() = viewModelScope.launch { publications.loadNextPage() }

Every accessor is scoped to the active profile and restarts on its own when the signed-in account or the active profile changes — see Content in the core module.

Downloads, reading progress and the reading list are kept per profile too — a session with no profile shares one set with every other such session on the device — and survive a relaunch. They do not survive a reinstall: the downloaded bytes and the rows recording who owns them both live in the app's container, so uninstalling takes them whatever profile they were keyed on.

5. Open an issue

CGS.reader().open(activity, issueId)

The reader takes it from there: an issue already on the device opens offline, one that is not is downloaded on the spot, behind the reader's own loading state. Its colors are the engine's own unless you pass a theme — see Reader in the core module. Downloading ahead of time — and following a download's progress — goes through CGS.download():

val item = CGS.download().issue(issueId)
item.start()

item.state.collect { state ->
when (state) {
is Download.Item.State.Downloaded -> showReadyToRead()
is Download.Item.State.Failed -> offerRetry()
else -> Unit
}
}

item.progress is a second StateFlow, from 0.0 to 1.0. Collect it separately for a progress bar: it moves while item.state stays on Downloading.

6. Or drop in a screen

CgsTheme(colorScheme = cgsColorScheme(isSystemInDarkTheme())) {
PublicationScreen(publicationId = id) { event ->
when (event) {
is PublicationEvent.ClickOnIssue -> CGS.reader().open(activity, event.issue.id, event.page)
// …
}
}
}

When no screen fits, compose your own from blocks — sections of issues or articles that load and recover on their own — described in the ui module.

Conventions across the API

  • One-shot calls that can fail return CgsResult — Success or Failure(CgsError). They do not throw.

  • Observed single items come back as a StateFlow of CgsStreamState, which can carry stale data alongside a failure.

  • Lists come back as a PaginatedResource: observe state, call loadNextPage() as the user nears the end.

  • Points in time are kotlin.time.Instant, never java.util.Date or java.time — one type across the whole API, with no desugaring to enable and nothing to opt into.

  • Exceptions are reserved for integration mistakes — a missing license, a call before init, a grid in a BlockPage without a limit. They are meant to be fixed, not caught.

  • Anything under a .internal package is not part of this contract and is absent from these pages.

All modules:

Link copied to clipboard

The catalogue, the session, the downloads and the reader — everything the SDK does that isn't drawing a screen of its own.

Link copied to clipboard

Ready-made Jetpack Compose screens, the blocks to build your own, and the design system that themes them.