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

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