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):

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.

Compose API

Dokka for CefriumView, CefriumViewState and CefriumNavigator.

Compose reference ->

SDK API reference

Full Javadoc for CefriumBrowser and the handlers.

Browse the API ->

Push & notifications

Web Notifications out of the box, Web Push, and native FCM -- what works and how to wire it.

Push guide ->

Working examples

cefrium-browser: full browser in View + Compose APIs. cefrium-sample: kiosk demos, the JS bridge, OSR.

cefrium-browser -> · cefrium-sample ->

Release parity

How Cefrium versions track CEF and Chromium upstream.

Releases ->

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/")