Quickstart
From an empty Android project to a CefriumBrowser
rendering a page, using the prebuilt artifacts. No Chromium build required.
1. Prerequisites
Android Studio with the Android SDK, JDK 25,
Gradle 9.7.1, AGP 9.4, and
compileSdk 37.
The SDK targets minSdk 29 and ships native code for
arm64-v8a and x86_64 - build for one of those ABIs (a
physical arm64 device or an x86_64 emulator).
These versions are a hard requirement from 0.8.0 on.
Chromium 152 and later compile their Java with --release 25, so the published
SDK, Compose wrapper, and Gradle plugin contain Java-25 (class-file 69)
bytecode. An older toolchain cannot read or dex them and the build fails with
Unsupported class file major version 69. On AGP 9 use the built-in
Kotlin support (drop org.jetbrains.kotlin.android) and legacy
jniLibs packaging. See the
0.8.0 release notes.
The engine's classes.jar is large (~50k classes), so dexing it
needs more than Gradle's default 512 MB heap. Set a 4 GB heap (and keep
AndroidX on, which Android Studio enables for new projects) in
gradle.properties:
# gradle.properties
org.gradle.jvmargs=-Xmx4g
android.useAndroidX=true
2. Add the repository
Cefrium is published to Maven Central (recommended —
mavenCentral() resolves it with no extra repository line, and every
version stays available) and mirrored on the Codeberg Maven registry
(latest version only). The snippets below list both, so either resolves it; drop the
codeberg.org/api/packages lines if you rely on Central. New Android Studio
projects use the Kotlin DSL by default; the Groovy form follows.
Pinning a fixed version in production? Use Maven Central. Central is immutable and keeps every version forever, so a pinned coordinate never disappears. The Codeberg mirror keeps only the latest version (donated-storage limits), so pinning an older version there breaks at the next release. Track-latest builds can use either; pinned / reproducible builds should resolve from Central.
settings.gradle.kts (Kotlin DSL)
pluginManagement {
repositories {
mavenCentral() // Cefrium is on Maven Central (recommended)
google()
// Optional Codeberg mirror (latest version only) -- omit if you use Maven Central:
maven("https://codeberg.org/api/packages/cefrium/maven")
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral() // Cefrium is on Maven Central (recommended)
// Optional Codeberg mirror (latest version only):
maven("https://codeberg.org/api/packages/cefrium/maven")
}
}
settings.gradle (Groovy)
pluginManagement {
repositories {
mavenCentral() // Cefrium is on Maven Central (recommended)
google()
// Optional Codeberg mirror (latest version only) -- omit if you use Maven Central:
maven { url 'https://codeberg.org/api/packages/cefrium/maven' }
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral() // Cefrium is on Maven Central (recommended)
// Optional Codeberg mirror (latest version only):
maven { url 'https://codeberg.org/api/packages/cefrium/maven' }
}
}
3. Apply the plugin and dependency
The com.cefrium Gradle plugin generates the R classes for
Chromium's many resource packages in your app, so the bundled engine resolves
its resources at runtime. Add it alongside the SDK dependency in your app module.
app/build.gradle.kts (Kotlin DSL)
plugins {
id("com.android.application")
id("com.cefrium") version "0.9.4"
}
dependencies {
implementation("com.cefrium:cefrium-sdk:0.9.4")
// Compose API (CefriumView + state). A new Empty Activity project already has
// Compose + Material3; omit this line if you only use the View-based CefriumBrowser.
implementation("com.cefrium:cefrium-compose:0.9.4")
// Runtime dependencies the engine expects.
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("androidx.mediarouter:mediarouter:1.7.0")
implementation("androidx.core:core:1.16.0")
implementation("androidx.browser:browser:1.8.0")
}
app/build.gradle (Groovy)
plugins {
id 'com.android.application'
id 'com.cefrium' version '0.9.4'
}
dependencies {
implementation 'com.cefrium:cefrium-sdk:0.9.4'
// Compose API (CefriumView + state). A new Empty Activity project already has
// Compose + Material3; omit this line if you only use the View-based CefriumBrowser.
implementation 'com.cefrium:cefrium-compose:0.9.4'
// Runtime dependencies the engine expects.
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.mediarouter:mediarouter:1.7.0'
implementation 'androidx.core:core:1.16.0'
implementation 'androidx.browser:browser:1.8.0'
}
cefrium-sdk is a single multi-ABI artifact
(arm64-v8a + x86_64); your APK or App Bundle ships only
the device's ABI - the engine's libcef.so is ~203 MB
(uncompressed) for arm64-v8a, so a single-ABI App Bundle download is
a fraction of the 238 MB two-ABI artifact. It includes the full codec set
(H.264, H.265, AAC, VP8, VP9, Opus); a royalty-free cefrium-sdk-libre
variant (VP8, VP9, Opus, Vorbis) is also published. The SDK's manifest contributes
the multi-process service declarations and required permissions through manifest
merging - you do not declare them.
The SDK bundles Chromium's copy of Guava's ListenableFuture, which
collides with the placeholder AndroidX pulls in transitively. Add one exclusion to
avoid a Duplicate class error:
// build.gradle.kts
configurations.all { exclude(group = "com.google.guava", module = "listenablefuture") }
// build.gradle (Groovy)
configurations.all { exclude group: "com.google.guava", module: "listenablefuture" }
4. Show a browser
No initialization code is required - no custom Application, no
Cefrium.initialize() call. The SDK auto-initializes the engine at app
launch (via a ContentProvider it contributes through manifest merging). Your
manifest only declares your own activity:
<!-- AndroidManifest.xml -->
<application ... >
<activity android:name=".MainActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
The whole app is one composable. rememberCefriumViewState ties the
engine to composition - it loads when shown and is released automatically when it
leaves the screen, so there is no onDestroy cleanup:
// MainActivity.kt
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import com.cefrium.compose.CefriumView
import com.cefrium.compose.rememberCefriumViewState
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val state = rememberCefriumViewState("https://cefrium.com/modern-web/")
CefriumView(state, Modifier.fillMaxSize())
}
}
}
Prefer classic Views?
CefriumBrowser.createWithSurface(activity) returns a
surfaceContainer you add to any ViewGroup (call
close() in onDestroy) - see
java-sample
for the Java and Kotlin View variants. Advanced: call
Cefrium.initialize(context) yourself to control init timing - still no
custom Application required.
5. Your first real browser screen
The view's state is observable, so a toolbar with the page title, a loading bar,
and a working system Back button is just reading
state.* and calling the navigator - the screen most first apps start
from:
// BrowserScreen.kt
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.cefrium.compose.CefriumView
import com.cefrium.compose.rememberCefriumNavigator
import com.cefrium.compose.rememberCefriumViewState
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BrowserScreen() {
val state = rememberCefriumViewState("https://cefrium.com/modern-web/")
val navigator = rememberCefriumNavigator()
// System Back walks the page history until there is nowhere left to go.
BackHandler(enabled = state.canGoBack) { navigator.navigateBack() }
Scaffold(
topBar = {
Column {
TopAppBar(
title = { Text(state.pageTitle ?: "Loading...", maxLines = 1) },
navigationIcon = {
IconButton(onClick = navigator::navigateBack,
enabled = state.canGoBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back")
}
},
)
if (state.isLoading) LinearProgressIndicator(Modifier.fillMaxWidth())
}
},
) { padding ->
CefriumView(state, Modifier.fillMaxSize().padding(padding), navigator)
}
}
Edge-to-edge (Android 15+): call
enableEdgeToEdge() in onCreate before
setContent; the Scaffold already insets its content, so
the page draws behind the system bars without overlapping your toolbar.
The same navigator drives the rest of the browser:
navigateForward(), reload(), loadUrl(),
evaluateJavaScript(), cookies, and the SSL / HTTP-auth handlers.
6. Run
Build and install on an arm64-v8a device or x86_64
emulator. The app loads cefrium.com/modern-web - your
first run is a live tour of which web-platform features the bundled engine
supports.
Going further
Common things first apps reach for next - all on the navigator (or
CefriumBrowser for the View path):
- App <-> page messaging -
navigator.evaluateJavaScript(...)and theonQuerybridge onCefriumView(window.cefriumQueryfrom page JS). - Cookies -
setCookie/getCookies/deleteCookies/flushCookies. - Request interception -
setRequestInterceptor(block, redirect, or inspect requests; powers the built-in ad-blocker). - SSL & HTTP auth -
setSslErrorHandler/setHttpAuthHandler(WebViewonReceivedSslError/onReceivedHttpAuthRequestparity). - Permissions, zoom, custom User-Agent, print-to-PDF --
setPermissionHandler,setZoomLevel,setUserAgentOverride,printToPdf.
Web notifications
A page's service worker posts notifications to the Android shade with
ServiceWorkerRegistration.showNotification() (the bare
new Notification() constructor is disabled on Android - use the
service worker). Grant the permission up front for an origin you own, no prompt:
import com.cefrium.CefriumBrowser
val navigator = rememberCefriumNavigator()
// once the page is loading, pre-grant notifications for your origin:
navigator.setContentSetting(
"https://your-app.example",
CefriumBrowser.PERMISSION_NOTIFICATIONS,
CefriumBrowser.SETTING_ALLOW,
)
Prefer to answer the request interactively? Use
setPermissionHandler. Web Push and production native FCM are covered
in the push & notifications guide.
Push & notifications
Web Notifications out of the box, Web Push, and native FCM -- what works and how to wire it.
Working examples
cefrium-browser: full browser in View + Compose APIs. cefrium-sample: kiosk demos, the JS bridge, OSR.
Headless rendering
For screenshots, PDFs, or server-side rendering with no window, construct a browser with a size and density and receive BGRA frames through a render handler:
val browser = CefriumBrowser(1080, 1920, resources.displayMetrics.density)
browser.setRenderHandler { width, height, pixels ->
// `pixels` is a direct ByteBuffer of width*height BGRA bytes.
}
browser.loadUrl("https://cefrium.com/")