Search Docs
Loading...
Skip to content

To v1.73

Version 1.73 introduces a major Android UI architecture change:

  • PhotoEditor, DesignEditor, VideoEditor, PostcardEditor and ApparelEditor are no longer the primary integration path.
  • A single Editor composable is now the foundation for all solutions.
  • The former solution UIs are now deprecated and instead provided as Starter Kits that you copy into your app and customize per your business logic.

This guide covers the Android migration path and lists the public API changes with before/after examples.

Choose Your Migration Path#

Use the starter kit that matches your previous solution:

Legacy ComposableStarter Kit GuideGitHub Repo
PhotoEditorPhoto Editorimgly/starterkit-photo-editor-android
DesignEditorDesign Editorimgly/starterkit-design-editor-android
VideoEditorVideo Editorimgly/starterkit-video-editor-android
PostcardEditorPostcard Editorimgly/starterkit-postcard-editor-android
ApparelEditorT-Shirt Designerimgly/starterkit-apparel-editor-android

Each starter kit includes:

  • A demo :app module to run the starter kit.
  • A reusable :starter-kit Android library module.
  • A configuration builder ({starter-kit-name}ConfigurationBuilder) class as the starting point.
  • Encapsulated callback/component setup that you can modify directly in your app codebase per your business logic.

Path B: Build a Fully Custom Editor#

We recommend using one of our starter kits, as they help with editor setup and provide a proven structure and architecture. However, you can still use the Editor composable without any configuration builder and provide your own scene creation, assets, and UI component configuration inline. Note that by default Editor launches an empty editor, meaning you have to configure all the components manually:

Editor(
license = "<license>",
userId = "<user-id>",
baseUri = "...".toUri(),
engineRenderTarget = EngineRenderTarget.SURFACE_VIEW,
configuration = {
EditorConfiguration.remember {
onCreate = { ... }
onExport = { ... }
dock = { ... }
...
overlay = { ... }
}
},
onClose = { ... },
)

Public API Changes#

1. Editor Entry Point#

Before:#

val engineConfiguration = EngineConfiguration.rememberForDesign(
license = "<license>",
userId = "<user-id>",
)
val editorConfiguration = EditorConfiguration.rememberForDesign()
DesignEditor(
engineConfiguration = engineConfiguration,
editorConfiguration = editorConfiguration,
onClose = onClose,
)

Now:#

Editor(
license = "<license>",
userId = "<user-id>",
configuration = {
// Note that DesignConfigurationBuilder comes with the starter kit and not available otherwise
EditorConfiguration.remember(::DesignConfigurationBuilder)
},
onClose = onClose,
)

What changed#

  • Added: ly.img.editor.Editor(...) composable.
  • Deprecated with DeprecationLevel.ERROR: PhotoEditor, DesignEditor, VideoEditor, PostcardEditor, ApparelEditor composables.
  • Deleted EngineConfiguration class.
  • Moved EditorConfiguration class from ly.img.editor to ly.img.editor.core.configuration.

2. Configuration Changes#

Before v1.73, configuration was split across two remember calls:

  • EngineConfiguration.remember(...) for engine configuration and engine callbacks.
  • EditorConfiguration.remember(...) for UI and event wiring.

From v1.73, those are unified into:

  • Editor(...) composable parameters for high level configurations.
  • EditorConfiguration.remember { ... } for all callbacks, UI and event wiring.

Below you can find the migration of all configuration options:

Before:#

val engineConfiguration = EngineConfiguration.remember(
license = "<license>",
userId = "<user-id>",
baseUri = "file:///android_asset/".toUri(),
renderTarget = EngineRenderTarget.SURFACE_VIEW,
onCreate = { ... },
onLoaded = { ... },
onExport = { ... },
onUpload = { asset: AssetDefinition, sourceType: UploadAssetSourceType -> asset },
onClose = { ... },
onError = { throwable: Throwable -> ... },
)
val editorConfiguration = EditorConfiguration.remember(
initialState = remember { EditorUiState() },
uiMode = EditorUiMode.SYSTEM,
assetLibrary = ...,
colorPalette = ...,
onEvent = { state: EditorUiState, event: EditorEvent -> /* Return updated EditorUiState */ },
overlay = { state: EditorUiState -> ... },
dock = { ... },
inspectorBar = { ... },
canvasMenu = { ... },
navigationBar = { ... },
)
DesignEditor(
engineConfiguration = engineConfiguration,
editorConfiguration = editorConfiguration,
onClose = { throwable: Throwable? -> ... },
)

Now:#

Editor(
license = "<license>",
userId = "<user-id>",
baseUri = "file:///android_asset/".toUri(),
engineRenderTarget = EngineRenderTarget.SURFACE_VIEW,
uiMode = EditorUiMode.SYSTEM,
configuration = {
EditorConfiguration.remember {
onCreate = { ... }
onLoaded = { ... }
onExport = { ... }
onClose = {
// hasUnsavedChanges: Boolean param is removed, you can use the following replacement:
val hasUnsavedChanges = editorContext.engine.editor.canUndo()
}
onEvent = { event: EditorEvent ->
// 1. state: EditorUiState param is removed
// 2. lambda does not return updated EditorUiState anymore
// Check the next section for more details on EditorUiState migration
}
onUpload = { asset: AssetDefinition, sourceType: UploadAssetSourceType -> asset }
onError = { throwable: Throwable -> ... }
colorPalette = { ... }
assetLibrary = { ... }
dock = { ... }
navigationBar = { ... }
inspectorBar = { ... }
canvasMenu = { ... }
bottomPanel = { ... }
overlay = {
// state: EditorUiState param is removed
}
}
},
onClose = { throwable: Throwable? -> ... },
)

What changed#

  • Moved all callbacks into unified EditorConfiguration.
  • Moved general configuration parameters (that is, license, engineRenderTarget) to Editor composable directly.
  • EditorConfiguration.remember is a builder DSL instead of regular composable function.
  • Removed hasUnsavedChanges: Boolean from the onClose callback. You can use engine.editor.canUndo() instead.
  • External editor UI state is completely removed: initialState is removed and EditorUiState usages are removed from onEvent and overlay lambdas. More details in the next section.

3. External Editor State#

In the legacy API, external UI state around editor operations was wired through EditorConfiguration<STATE> and ly.img.editor.Events.

Before:#

This was the typical legacy flow:

  1. Provide initialState in EditorConfiguration.remember(...), for example initialState = remember { EditorUiState() }.
  2. Send events from engine callbacks using ly.img.editor.Events, for example ShowLoading / HideLoading.
  3. Capture and reduce events in onEvent, commonly via EditorDefaults.onEvent(...).
  4. Render from the updated state in overlay.
val engineConfiguration = EngineConfiguration.remember(
license = "<license>",
onCreate = {
try {
editorContext.eventHandler.send(ShowLoading)
if (editorContext.engine.scene.get() == null) {
val scene = editorContext.engine.scene.create()
val page = editorContext.engine.block.create(DesignBlockType.Page)
editorContext.engine.block.appendChild(parent = scene, child = page)
}
} finally {
editorContext.eventHandler.send(HideLoading)
}
},
)
val editorConfiguration = EditorConfiguration.remember(
initialState = remember { EditorUiState() },
onEvent = { state: EditorUiState, event: EditorEvent ->
EditorDefaults.onEvent(editorContext.activity, state, event)
},
overlay = { state: EditorUiState ->
if (state.isLoading) {
// render loading UI
}
},
)
DesignEditor(
engineConfiguration = engineConfiguration,
editorConfiguration = editorConfiguration,
onClose = { throwable: Throwable? -> ... },
)

Now:#

Use plain Compose state showLoading:

var showLoading by remember { mutableStateOf(false) }
Editor(
license = "<license>",
configuration = {
EditorConfiguration.remember {
onCreate = {
try {
// Set value from callbacks and components
showLoading = true
if (editorContext.engine.scene.get() == null) {
val scene = editorContext.engine.scene.create()
val page = editorContext.engine.block.create(DesignBlockType.Page)
editorContext.engine.block.appendChild(parent = scene, child = page)
}
} finally {
showLoading = false
}
}
// No onEvent block required.
overlay = {
EditorComponent.remember {
id = { EditorComponentId("my.package.overlay") }
decoration = {
// Read value in compose callbacks
if (showLoading) {
// render loading UI
}
}
}
}
}
},
onClose = { throwable: Throwable? -> ... },
)

4. Editor Defaults#

Before:#

  • When using EngineConfiguration.remember(...), callbacks other than onCreate had generic default implementations (onLoaded, onExport, onUpload, onClose, onError).
  • onCreate was commonly provided by solution helpers such as EngineConfiguration.rememberForDesign(...) (and equivalent helpers for other solutions).
  • UI component defaults were also solution-driven. For example, EditorConfiguration.rememberForDesign(...) provided default component wiring via helpers like Dock.rememberForDesign(), NavigationBar.rememberForDesign(), InspectorBar.remember(), and CanvasMenu.remember().

Now:#

  • There is no default onCreate. If you do not provide one, the editor creates a blank scene with a single 1080x1080 page only to ensure something is visible.
  • Default onExport does nothing.
  • Default onClose directly closes the editor.
  • Editor does not show any overlay content, that is, loading indicators and dialogs. Overlay rendering must be provided manually.
  • All components (that is, Dock, Overlay) are not provided by default at all. You have to explicitly provide instances.
  • Note that components containing list of buttons (Dock, NavigationBar, InspectorBar and CanvasMenu) are by default empty. You have to provide your own listBuilder implementations:
Editor(
license = "<license>",
configuration = {
EditorConfiguration.remember {
onCreate = {
// Must be implemented
}
onExport = {
// Must be implemented
}
dock = {
Dock.remember {
listBuilder = {
Dock.ListBuilder.remember {
add { Dock.Button.rememberAdjustments() }
add { Dock.Button.rememberFilter() }
add { Dock.Button.rememberEffect() }
}
}
}
}
overlay = {
// Render overlays if you want to render loading indicators, confirmation dialogs, export dialogs etc.
EditorComponent.remember {
id = { EditorComponentId("my.package.overlay") }
decoration = {
// Render custom overlay
}
}
}
}
},
onClose = { throwable: Throwable? -> ... },
)

5. Component API#

Parameter-heavy remember(...) calls were common across all components. Now they are replaced with DSL builders:

Before:#

Dock.Button.remember(
id = EditorComponentId("my.button"),
vectorIcon = { IconPack.AddText },
text = { "Text" },
onClick = { /* ... */ },
)

Now:#

Dock.Button.remember {
id = { EditorComponentId("my.button") }
vectorIcon = { IconPack.AddText }
textString = { "Text" }
onClick = { /* ... */ }
}

A new configuration of Modifier is added to all components:

Dock.remember {
modifier = { Modifier.shadow(5.dp) }
horizontalArrangement = { Arrangement.Center }
listBuilder = {
Dock.ListBuilder.remember {
add { Dock.Button.rememberAdjustments() }
}
}
}
  • Dock.Button, InspectorBar.Button, NavigationBar.Button and CanvasMenu.Button components are combined into a single Button component.
  • CanvasMenu.Divider component is replaced with a generic Divider component.
  • A new Timeline component is introduced that provides powerful editing tools for editors that work with videos.
  • A new bottomPanel configuration option is introduced in EditorConfiguration that allows rendering just above the dock. For instance, this configuration can be used to render Timeline in editors that work with videos.
Editor(
license = "<license>",
configuration = {
EditorConfiguration.remember {
bottomPanel = { Timeline.remember() }
}
},
onClose = { throwable: Throwable? -> ... },
)

6. EditorContext Changes#

Configuration options used to be exposed as direct EditorContext properties (for example colorPalette, assetLibrary, dock, overlay, etc.).
These are replaced by a unified configuration: StateFlow<EditorConfiguration?> property, which allows both reading the current value and observing value changes of EditorConfiguration:

Before:#

onCreate = {
// Example in a regular callback
val assetLibrary = editorContext.assetLibrary
}
dock = {
// Example in a composable callback
val assetLibrary = editorContext.assetLibrary
}

Now:#

onCreate = {
// Example in a regular callback, accessing current value
val assetLibrary = editorContext.configuration.value?.assetLibrary
// Example in a regular callback, observing the value
editorContext.coroutineScope.launch {
editorContext.configuration.collect {
val mostRecentAssetLibrary = it?.assetLibrary
}
}
}
dock = {
// Example in composable callback, accessing current value
val assetLibrary = editorContext.configuration.value?.assetLibrary
// Example in a composable callback, observing the value
val mostRecentConfiguration by editorContext.configuration.collectAsState()
val mostRecentAssetLibrary = mostRecentConfiguration?.assetLibrary
}

A new coroutineScope property is introduced and should be used to launch coroutines from editor callbacks/components.

  • The coroutine scope is always alive while the editor is running.
  • It also survives configuration changes.
editorContext.coroutineScope.launch {
// long-running editor work
}

7. EditorState Changes#

EditorState has been expanded and now contains many more valuable properties.

Before:#

data class EditorState(
val canvasInsets: Rect = Rect.Zero,
val activeSheet: SheetType? = null,
val isTouchActive: Boolean = false,
val isHistoryEnabled: Boolean = true,
val viewMode: EditorViewMode = EditorViewMode.Edit(),
)

Now:#

@Immutable
data class EditorState(
val insets: Insets = Insets.Zero, // equivalent to canvasInsets before
val extraInsets: Insets = Insets.Zero,
val activeSheet: SheetType? = null,
val activeSheetState: SheetState? = null,
val isTouchActive: Boolean = false,
val isHistoryEnabled: Boolean = true,
val isBackHandlerEnabled: Boolean = false,
val viewMode: EditorViewMode = EditorViewMode.Edit(),
val dimensions: Dimensions = Dimensions(),
val minVideoDuration: Duration? = null,
val maxVideoDuration: Duration? = null,
)
data class Dimensions(
val editor: Size = Size.Zero,
val navigationBar: Size = Size.Zero,
val bottomPanel: Size = Size.Zero,
val dock: Size = Size.Zero,
val inspectorBar: Size = Size.Zero,
)

8. New EditorEvents#

The new architecture adds several public events for runtime editor updates:

  1. EditorEvent.Insets.SetExtra

Sets extra canvas insets on top of what is already calculated based on system status and navigation bars, IMG.LY dock and navigation bars, currently open sheet, timeline, system keyboard, etc.:

editorContext.eventHandler.send(
EditorEvent.Insets.SetExtra(
insets = Insets(bottom = 24.dp),
),
)
  1. EditorEvent.ApplyForceCrop

An event for enforcing crop configuration to a specific designBlock:

// Force 1:1, 16:9 or 9:16 aspect ratios to the block
val configuration = ForceCropConfiguration(
sourceId = "ly.img.crop.presets",
presetId = "aspect-ratio-1-1",
presetCandidates = listOf(
ForceCropPresetCandidate(
sourceId = "ly.img.crop.presets",
presetId = "aspect-ratio-16-9",
),
ForceCropPresetCandidate(
sourceId = "ly.img.crop.presets",
presetId = "aspect-ratio-9-16",
),
),
mode = ForceCropMode.IfNeeded(threshold = 0.01f),
)
editorContext.eventHandler.send(
EditorEvent.ApplyForceCrop(
designBlock = block,
configuration = configuration,
),
)

See Force Crop for more details.

  1. EditorEvent.ApplyVideoDurationConstraints

An event for applying video duration constraints:

editorContext.eventHandler.send(
EditorEvent.ApplyVideoDurationConstraints(
minDuration = 2.seconds,
maxDuration = 30.seconds,
),
)