Version 1.73 introduces a major Android UI architecture change:
PhotoEditor,DesignEditor,VideoEditor,PostcardEditorandApparelEditorare no longer the primary integration path.- A single
Editorcomposable 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#
Path A: Adopt a Starter Kit (Recommended)#
Use the starter kit that matches your previous solution:
| Legacy Composable | Starter Kit Guide | GitHub Repo |
|---|---|---|
PhotoEditor | Photo Editor | imgly/starterkit-photo-editor-android |
DesignEditor | Design Editor | imgly/starterkit-design-editor-android |
VideoEditor | Video Editor | imgly/starterkit-video-editor-android |
PostcardEditor | Postcard Editor | imgly/starterkit-postcard-editor-android |
ApparelEditor | T-Shirt Designer | imgly/starterkit-apparel-editor-android |
Each starter kit includes:
- A demo
:appmodule to run the starter kit. - A reusable
:starter-kitAndroid 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,ApparelEditorcomposables. - Deleted
EngineConfigurationclass. - Moved
EditorConfigurationclass fromly.img.editortoly.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) toEditorcomposable directly. EditorConfiguration.rememberis a builder DSL instead of regular composable function.- Removed
hasUnsavedChanges: Booleanfrom theonClosecallback. You can useengine.editor.canUndo()instead. - External editor UI state is completely removed:
initialStateis removed andEditorUiStateusages are removed fromonEventandoverlaylambdas. 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:
- Provide
initialStateinEditorConfiguration.remember(...), for exampleinitialState = remember { EditorUiState() }. - Send events from engine callbacks using
ly.img.editor.Events, for exampleShowLoading/HideLoading. - Capture and reduce events in
onEvent, commonly viaEditorDefaults.onEvent(...). - Render from the updated
stateinoverlay.
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 thanonCreatehad generic default implementations (onLoaded,onExport,onUpload,onClose,onError). onCreatewas commonly provided by solution helpers such asEngineConfiguration.rememberForDesign(...)(and equivalent helpers for other solutions).- UI component defaults were also solution-driven. For example,
EditorConfiguration.rememberForDesign(...)provided default component wiring via helpers likeDock.rememberForDesign(),NavigationBar.rememberForDesign(),InspectorBar.remember(), andCanvasMenu.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
onExportdoes nothing. - Default
onClosedirectly 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,InspectorBarandCanvasMenu) are by default empty. You have to provide your ownlistBuilderimplementations:
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.ButtonandCanvasMenu.Buttoncomponents are combined into a singleButtoncomponent.CanvasMenu.Dividercomponent is replaced with a genericDividercomponent.- A new
Timelinecomponent is introduced that provides powerful editing tools for editors that work with videos. - A new
bottomPanelconfiguration option is introduced inEditorConfigurationthat allows rendering just above the dock. For instance, this configuration can be used to renderTimelinein 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:#
@Immutabledata 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:
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), ),)EditorEvent.ApplyForceCrop
An event for enforcing crop configuration to a specific designBlock:
// Force 1:1, 16:9 or 9:16 aspect ratios to the blockval 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.
EditorEvent.ApplyVideoDurationConstraints
An event for applying video duration constraints:
editorContext.eventHandler.send( EditorEvent.ApplyVideoDurationConstraints( minDuration = 2.seconds, maxDuration = 30.seconds, ),)