Putting a photo editor in an Android app means writing a crop tool, a filter pipeline, an undo stack and an export path. Then you maintain all four. CreativeEditor SDK ships that as a library module. You drop it into a project you already have, and what’s left is the Gradle wiring and a decision about where the editor opens from.

This tutorial covers both. We’re going to take the Photo Editor starter kit, add it to an existing app, and launch it from a button. At the end you’ve got a full screen editor with adjustments, filters, crop, text and stickers. Export hands the finished image to the Android share sheet.

What you will need

  • The latest Android Studio
  • Kotlin 2.1.10, the version the starter kit is built with
  • Android Gradle Plugin 8.11.0 with Gradle 8.13
  • JDK 17, which that plugin version requires
  • Android 7.0 or later (minSdk 24)
  • An Android app to integrate into

You don’t need a license key to get going. The editor runs in evaluation mode, which watermarks the exported image.

Step 1: Pull the starter kit into your project

The Photo Editor starter kit is an Android library module. You copy it into your codebase instead of pulling it from a repository. That means everything in it is yours to edit afterwards: the tool list, the export handler, the scene it opens with.

Run this from your application root directory:

repo="starterkit-photo-editor-android"
version="1.82.1"
curl -L "https://codeload.github.com/imgly/${repo}/tar.gz/refs/heads/v${version}" | tar -xz --strip-components=1 "${repo}-${version}/starter-kit"

That extracts a starter-kit/ directory alongside your app/ module.

Step 2: Wire the module into Gradle

First, declare the module at the bottom of settings.gradle.kts, next to the include lines already there:

include(":starter-kit")

The same file already carries a dependencyResolutionManagement block, above those include lines. Add the IMG.LY Maven repository to the repositories list inside it, so Gradle can resolve the editor the module depends on:

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven {
            name = "IMG.LY Artifactory"
            url = uri("https://maven.img.ly/maven")
            mavenContent {
                includeGroup("ly.img")
            }
        }
    }
}

The includeGroup("ly.img") line keeps that repository scoped to IMG.LY artifacts. Nothing else in your build resolves through it.

The starter kit is built with Jetpack Compose. If your project doesn’t already apply the Compose compiler plugin, add it to the root build.gradle.kts:

plugins {
    // Existing plugins here
    id("org.jetbrains.kotlin.plugin.compose") version "2.1.10" apply false
}

That version tracks Kotlin: keep the Compose plugin version and the Kotlin version the same.

Then depend on the module from your app module:

dependencies {
  implementation(project(":starter-kit"))
}

The editor arrives transitively. Open starter-kit/build.gradle.kts and you’ll see the coordinate that does the work:

dependencies {
    api("ly.img:editor:1.82.1")
    // Required for enableEdgeToEdge
    implementation("androidx.activity:activity-ktx:1.9.0")
}

Sync the project through File -> Sync Project With Gradle Files and everything is on the classpath.

Step 3: Launch the editor from your own UI

The starter kit ships an EditorActivity for apps on classic Activity navigation. The module’s manifest declares it, and that manifest merges into your app, so there is nothing to register. Starting it is an ordinary intent, which means a button in a layout you already have is enough.

Add the button to that layout:

<Button
    android:id="@+id/btn_launch_editor"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Launch Editor" />

Then wire it up in the activity that inflates the layout:

import android.content.Intent
import android.os.Bundle
import android.widget.Button
import androidx.activity.ComponentActivity
import ly.img.editor.configuration.photo.EditorActivity

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        findViewById<Button>(R.id.btn_launch_editor).setOnClickListener {
            val intent = Intent(this, EditorActivity::class.java)
            startActivity(intent)
        }
    }
}

Run the app and the button opens the editor. Here’s the activity behind it, so you can see how little is going on:

package ly.img.editor.configuration.photo

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import ly.img.editor.Editor
import ly.img.editor.core.configuration.EditorConfiguration
import ly.img.editor.core.configuration.remember

/**
 * Encapsulated editor to be used in legacy activity navigation.
 * Delete this file if you are using jetpack compose navigation.
 */
class EditorActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // This is required to remove the default action bar on top.
        setTheme(android.R.style.Theme_Material_Light_NoActionBar)
        // This is required, so that the editor is displayed full screen on relatively older devices.
        enableEdgeToEdge()
        setContent {
            Editor(
                license = null, // pass null or empty for evaluation mode with watermark
                configuration = {
                    EditorConfiguration.remember(::PhotoConfigurationBuilder)
                },
                onClose = {
                    // Finish the activity, potentially handle errors.
                    finish()
                },
            )
        }
    }
}

The Editor composable takes eight parameters, and the activity sets only three of them. license is null, which selects evaluation mode. configuration points at PhotoConfigurationBuilder, the class that gathers every decision the kit makes. onClose runs when the user leaves, and it receives the error if there was one. That’s where you pop your own screen or show a message.

If your app is on Compose navigation, skip the activity and the button, and add the composable as a destination instead:

import androidx.compose.runtime.Composable
import ly.img.editor.Editor
import ly.img.editor.configuration.photo.PhotoConfigurationBuilder
import ly.img.editor.core.configuration.EditorConfiguration
import ly.img.editor.core.configuration.remember

// onClose should pop screen from the backstack of jetpack compose navigation
@Composable
fun EditorScreen(onClose: (Throwable?) -> Unit) {
    Editor(
        license = null, // pass null or empty for evaluation mode with watermark
        configuration = {
            EditorConfiguration.remember(::PhotoConfigurationBuilder)
        },
        onClose = onClose,
    )
}

You can then delete EditorActivity.kt from the module, together with the <activity> entry that names it in starter-kit/src/main/AndroidManifest.xml. Leave that entry behind and the merged manifest points at a class that is gone.

What you get for those three steps

The dock along the bottom carries adjustments, filters, effects, blur, crop, a text library, shapes and stickers. Its contents are listed in component/Dock.kt, one line per button. The dock hides itself while the user is in preview mode.

The bar along the top holds close on the left, then undo, redo, a preview toggle and export on the right. That list lives in component/NavigationBar.kt and follows the same pattern.

Export renders the scene to a PNG, writes it to a file and passes the file to the share sheet. The inspector bar, the canvas menu and the overlay each have their own file next to those two. Overlay.kt is the hook for anything drawn on top of the editor. As shipped it’s a single call that hands the drawing to the library’s default Overlay(). The confirmation shown when someone closes with unsaved edits is wired up elsewhere, in PhotoConfigurationBuilder.onClose, which calls showConfirmationOrCloseEditor().

Changing what it does

Because the module is in your codebase, you configure it by editing the files you just extracted. Here is where the common changes live.

The image it opens with. The kit starts on a sample image from the IMG.LY CDN. The scene setup sits in callback/OnCreate.kt. CE.SDK will load a scene from several sources:

import androidx.core.net.toUri

// Load from an image uri - creates a new scene with the image
editorContext.engine.scene.createFromImage(imageUri = "https://example.com/photo.jpg".toUri())

// Load from a template archive - restores a previously saved project
editorContext.engine.scene.load(sceneUri = "https://example.com/template.zip".toUri())

// Create a blank canvas - starts with an empty design scene
editorContext.engine.scene.create()

// Load from a scene file - restores a scene from .scene file
editorContext.engine.scene.load(sceneUri = "https://example.com/saved.scene".toUri())

PhotoConfigurationBuilder has no uri parameter today, and onCreateScene() hard-codes the sample URL. To open on the user’s own image, edit that call, or add a parameter of your own to carry the uri. Either way it ends up in getOrCreateSceneFromImage(imageUri = ...), the helper in the same file. That helper reuses the scene the engine already holds, and creates a new one from the image if there isn’t one.

What export produces. callback/OnExport.kt splits into the call that renders bytes and the callback that decides their fate:

suspend fun PhotoConfigurationBuilder.onExportByteBuffer(): ByteBuffer = export(
    block = requireNotNull(editorContext.engine.scene.get()),
    mimeType = MimeType.PNG,
)

suspend fun PhotoConfigurationBuilder.onPostExport(byteBuffer: ByteBuffer) {
    val file = writeToFile(byteBuffer = byteBuffer, mimeType = MimeType.PNG)
    shareFile(file = file, mimeType = MimeType.PNG)
}

MimeType.PNG appears three times there and a switch to MimeType.JPEG has to change all three. The first one picks the format the engine renders, the second gives the written file its extension, and the third sets the type the share sheet hands to the receiving app. Edit onPostExport further to upload the buffer to your backend, or write it to the gallery instead of sharing it.

Where assets load from. By default the editor reads its stickers, filters and typefaces from the IMG.LY CDN. Pass baseUri to the same Editor call to read them from somewhere else, the app’s own assets directory included:

import androidx.compose.runtime.Composable
import androidx.core.net.toUri
import ly.img.editor.Editor
import ly.img.editor.configuration.photo.PhotoConfigurationBuilder
import ly.img.editor.core.configuration.EditorConfiguration
import ly.img.editor.core.configuration.remember

@Composable
fun EditorScreen(onClose: (Throwable?) -> Unit) {
    Editor(
        license = null, // pass null or empty for evaluation mode with watermark
        baseUri = "file:///android_asset/assets".toUri(), // this points to android assets
        configuration = {
            EditorConfiguration.remember(::PhotoConfigurationBuilder)
        },
        onClose = onClose,
    )
}

Whatever you point that at holds one directory per asset source, each with a content.json the kit loads as $baseUri/$id/content.json. Copying the bundle’s inner assets/ folder into your app module’s src/main/assets/ produces paths like src/main/assets/assets/ly.img.sticker/content.json, which is why the URI above ends in /assets.

Before you ship

Pass a license key instead of null, and the watermark goes away. Request a trial license through the CE.SDK Android guide.

Serve the assets yourself. The IMG.LY CDN is for development and evaluation only, so a production app has to host the asset bundle on its own infrastructure or ship it inside the APK. The Serve Assets guide covers downloading the bundle, setting baseUri and dropping the sources you don’t use. Bundling the assets in the APK does not take the CDN out of the picture altogether: under a file:// or app-asset baseUri, fallback font files, the ones that cover characters the chosen typeface is missing, still load from the IMG.LY CDN.

Where to go next