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 21 (Gradle 8.9 does
not run on JDK 22+), AGP 8.6+, and compileSdk 35.
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).
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 the Codeberg Maven registry. New Android Studio projects use the Kotlin DSL by default; the Groovy form follows.
settings.gradle.kts (Kotlin DSL)
pluginManagement {
repositories {
maven("https://codeberg.org/api/packages/cefrium/maven")
google()
mavenCentral()
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven("https://codeberg.org/api/packages/cefrium/maven")
}
}
settings.gradle (Groovy)
pluginManagement {
repositories {
maven { url 'https://codeberg.org/api/packages/cefrium/maven' }
google()
mavenCentral()
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
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.6.0"
}
dependencies {
implementation("com.cefrium:cefrium-sdk:0.6.0")
// 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.6.0")
// 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.6.0'
}
dependencies {
implementation 'com.cefrium:cefrium-sdk:0.6.0'
// 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.6.0'
// 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/")