← Blog

Android video editor SDK for Views and Jetpack Compose

· 7 min read · androidkotlinjetpack-composevideo-editing

Picture the ticket: "Let users edit their clip before posting." An Android engineer reads it, opens the Media3 docs, and finds solid tools for transcoding, trimming and effects, but nothing a user can see or touch. The timeline, the text tool and the music track all start from an empty Activity. This post shows the shorter route: adding Nitrakit, an Android video editor SDK for Kotlin, to a classic View layout or a Jetpack Compose screen, and the Android-specific details that matter on the way to Play: lifecycle, R8 and release signing. The code is copied from the Android guide.

A video editor for your Android app

Media3 processes video; users need an editor

Media3 Transformer is Google's API for editing media in code. It's the right tool when your app decides the edit. When the user decides it, you also need the screen: clips to drag and trim, text to place, a preview that tracks every change, undo. On Android it also has to behave on every chipset and screen size your users own. Build or buy an in-app video editor weighs that work for whoever owns the roadmap.

Nitrakit is that screen, delivered as two Android views. Media3 can keep doing whatever it already does in your app, like playback in the feed.

Add the Android video editor SDK: one Gradle line

The artifact lives on Maven Central, which your build already resolves:

dependencies {
    implementation("dev.nitrakit:nitrakit:0.2.0")
}

You need minSdk 26 (Android 8.0) and Kotlin. Native libraries are included for ARM and x86 CPUs. The SDK asks for no permissions of its own.

Start a project

An Editor is the project in memory: tracks, clips, text and the undo history. Construct it with the output width, height and frame rate, then put the user's video on a track:

val editor = Editor(1080u, 1920u, 30u, 1u)

val media = editor.addMedia(file, id = assetId)
val track = editor.apply(Edit.AddTrack(TrackKind.VISUAL, true))
editor.apply(Edit.InsertMedia(track, media.source, 0, 0, media.durationUs))

assetId should be the id your own storage uses, such as an object key. Projects only ever name media this way, which keeps them portable between phones. The file is copied in on add, so the user can start cutting straight away. Times are in microseconds throughout.

With Views: an Activity in a dozen lines

NitrakitPreviewView renders the video and handles touch on layers. NitrakitTimelineView carries the timeline and every tool. Hand each the same Editor and add them to any layout:

val preview = NitrakitPreviewView(this).apply { this.editor = editor }
val dock = NitrakitTimelineView(this).apply { this.editor = editor }
val dockHeight = (400 * resources.displayMetrics.density).toInt()  // 400 dp

setContentView(LinearLayout(this).apply {
    orientation = LinearLayout.VERTICAL
    addView(preview, LinearLayout.LayoutParams(MATCH_PARENT, 0, 1f))
    addView(dock, LinearLayout.LayoutParams(MATCH_PARENT, dockHeight))
})

They're constructed in Kotlin rather than inflated from layout XML. Separate views also mean a screen can show only part of the editor, for instance just the preview under your own buttons; the layouts page has the options.

Jetpack Compose video editor with AndroidView

Compose hosts the same two views through AndroidView. The factory lambdas create them once; onRelease detaches them when they leave composition:

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import dev.nitrakit.Editor
import dev.nitrakit.ui.NitrakitPreviewView
import dev.nitrakit.ui.NitrakitTimelineView

@Composable
fun EditorScreen(editor: Editor) {
    Column(Modifier.fillMaxSize().background(Color.Black)) {
        AndroidView(
            factory = { NitrakitPreviewView(it).apply { this.editor = editor } },
            onRelease = { it.editor = null },
            modifier = Modifier.fillMaxWidth().weight(1f),
        )
        AndroidView(
            factory = { context ->
                NitrakitTimelineView(context).apply {
                    this.editor = editor
                    onRequest = { request -> null }  // your pickers: step 5
                }
            },
            onRelease = { it.editor = null },
            modifier = Modifier.fillMaxWidth().height(400.dp),
        )
    }
}

This is an in-place composable, not a separate Activity you launch and wait on. It sits in your NavHost like any other destination, under your own top bar and inside your theme.

Who owns the Editor

The Editor holds native resources, so it needs one clear owner and one clear end. In a Views screen, the Activity is the owner: detach both views and close the editor in onDestroy.

override fun onDestroy() {
    dock.editor = null
    preview.editor = null
    editor.close()
    super.onDestroy()
}

In Compose, recomposition must never build a new one, so keep it out of the composable. A ViewModel is the natural home: create the Editor there and call editor.close() when the ViewModel is cleared. The composable then only receives it as a parameter, as above.

Going to the background is handled for you. The preview remembers its position and the timeline its state. The playback object behind the preview is recreated when the app returns, and the preview's onPlayer callback hands you the new one if your own play buttons hold a reference.

The SDK opens no gallery or file browser of its own. When the user taps Add media, or adds music or a sticker, the request reaches your onRequest handler: a suspend function that starts on the main thread. Open the system gallery, a screen of your licensed music, or the user's cloud uploads, and return what they chose:

dock.onRequest = { request ->
    when (request.kind) {
        RequestType.ADD_MEDIA, RequestType.ADD_OVERLAY,
        RequestType.ADD_AUDIO, RequestType.ADD_STICKER -> {
            val file = pickMedia() // your picker, copied to a File
            if (file == null) RequestResponse.Cancel
            else RequestResponse.Source(editor.addMedia(file, id = startUpload(file)).source)
        }
        else -> null // built-in picker
    }
}

With Android's photo picker, users choose individual videos and your app doesn't request access to their whole library. Copy the chosen Uri to a File and pass it on. For fonts, colours, text styles and filters, returning null falls back to screens the SDK provides, so media is the only thing you must build on day one. The request reference lists every case.

Projects that reopen on another phone

Save the project as JSON whenever you like; it carries your ids, not file paths:

val json = editor.toJson()

val reopened = Editor(json)
reopened.loadMedia()   // from the device, else through NitrakitMedia.resolve

A user who switches phones, or reinstalls, opens the draft and the SDK asks your app where each missing id lives. Your suspend resolver returns a URL, signed if your bucket requires it, and download progress shows on the clips:

NitrakitMedia.resolve = { id -> MediaLocation.Remote(api.signedUrl(id)) }   // or Local(file)

Assign it once at start-up, Application.onCreate for example. A change listener on the editor gives you autosave. Projects and media goes into retries and uploads still in flight.

Export without blocking the UI

export returns a job immediately and renders on its own thread:

val job = editor.export(outputPath, editor.exportSettingsFor(1080u), object : ExportListener {
    override fun progress(fraction: Double, frames: ULong, total: ULong) {
        runOnUiThread { /* update your progress bar */ }
    }
    override fun finished(result: ExportResult, message: String) {
        // DONE: the file is at your path. CANCELLED or FAILED: no file (message says why).
    }
})
// job.cancel()

Close the ExportJob when it finishes. The finished MP4 is an ordinary file, ready for MediaStore, a share intent, or a WorkManager upload. The Export page lists the settings. Speech in the video can also become captions timed to each word, from your own speech-to-text provider; see Text and captions.

Brand it, and follow the system theme

dock.theme = EditorTheme(
    background = "#101418",
    bar = "#0b0e11",
    accent = "#2ee6a6",
    font = "Brand Sans",
    clips = EditorTheme.ClipColors(video = "#3355ff", audio = "#18a058"),
)

Set only the fields you care about. The editor won't switch to dark on its own: when the configuration changes, assign the theme your design system uses for that mode. Beyond colour, you can drop tools, add your own, change icons and translate every label, while the app runs. Customisation has the details.

Shipping to Play: R8, signing and size

Three checks before the release build.

R8. Minified builds need one keep rule, because the SDK's native code calls its Kotlin classes by name:

-keep class dev.nitrakit.** { *; }

The license key. Debug builds and internal testing work without one. A release build sets the key at start-up:

class App : Application() {
    override fun onCreate() {
        super.onCreate()
        val status = NitrakitLicense.set(this, "nk2.…")
        if (status.problems.isNotEmpty()) Log.w("App", "Nitrakit: ${status.problems}")
    }
}

The key is matched against your package name and the SHA-256 fingerprint of the certificate that signs the release. With Play App Signing that's the app signing key shown in Play Console (add the upload key too if you test upload-signed builds); the License key page walks through it.

Size. Publish an Android App Bundle, and each phone downloads only the native library for its own CPU. Current numbers are on the App size page.

Next steps for your Android team

Drop EditorScreen into a test destination, run it on an emulator with a sample clip, and see how it sits next to your own screens. Prefer to see it without Gradle first? Open the demo in a browser. The Android guide has the full integration, and the Android page is the summary to forward to your CTO. If iOS is next on your list, read the iOS version; if you'd rather share one codebase, there's the React Native guide.

When a release date is close, talk to us about a key. Gradle or lifecycle questions in the meantime go to Discord.

Get started