Android video editor SDK for Views and Jetpack Compose
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.

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.
Getting media in from the gallery or your backend
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.