In this example, we will show you how to configure the callbacks of various editor events for the mobile editor.
Configuration#
For demonstration purposes, let’s create a data class that holds the state of the editor:
data class State( val isLoading: Boolean = false, val isCloseConfirmationDialogVisible: Boolean = false, val error: Throwable? = null,)Also, let’s add an instance of it as a mutable state:
var state by remember { mutableStateOf(State()) }All the callback configurations are part of the EditorConfiguration class. Note that all the callbacks are within EditorScope, meaning you can access editorContext in all of them.
onCreate- the callback that is invoked when the editor is created. This is the main initialization block of both the editor and engine. Normally, you should load or create a scene as well as prepare asset sources in this block. We recommend that you check the availability of the scene before creating/loading a new scene since a recreated scene may already exist if the callback is invoked after a process recreation. In this example, we create a scene with a single square page, update the state to show a loading in theoverlaywhile creating the scene and also update one of the editor settings.
onCreate = { state = state.copy(isLoading = true) try { val scene = editorContext.engine.scene.create() val page = editorContext.engine.block.create(DesignBlockType.Page) editorContext.engine.block.setWidth(block = page, value = 1080F) editorContext.engine.block.setHeight(block = page, value = 1080F) editorContext.engine.block.appendChild(parent = scene, child = page) editorContext.engine.editor.setSettingEnum(keypath = "touch/pinchAction", value = "Scale") // Add ImageUploads asset source to demonstrate onUpload editorContext.engine.asset.addLocalSource( sourceId = AssetSourceType.ImageUploads.sourceId, supportedMimeTypes = listOf(AssetSourceType.ImageUploads.mimeTypeFilter), ) } finally { state = state.copy(isLoading = false) }}onLoaded- the callback that is invoked when the editor is loaded and ready to be used. The callback is invoked right afteronCreatewhen launching the editor for the first time or after process recreation. The callback is not invoked after configuration changes. It is best to register callbacks, collect flows returned by the engine and apply editor settings in this callback. Note that the “load” coroutine job will survive configuration changes and will be cancelled only if the editor is closed or the process is killed when in the background. In this example, we observe the editor history and the edit mode.
onLoaded = { coroutineScope { launch { editorContext.engine.editor .onStateChanged() .map { editorContext.engine.editor.getEditMode() } .distinctUntilChanged() .collect { Toast.makeText(editorContext.activity, "Edit mode is updated to $it!", Toast.LENGTH_SHORT).show() } } }}onExport- the callback that is invoked when the export button is clicked. You may want to call one of the export functions in this callback. Note that the “export” coroutine job will survive configuration changes and will be cancelled only if the editor is closed or the process is killed when in the background. In this example, we export the scene to a PDF file and write the content of it to a temporary file. We also update the state to show a loading in theoverlaywhile the export is running. Finally, we update the state to show an error dialog in theoverlayif the export fails.
onExport = { val engine = editorContext.engine state = state.copy(isLoading = true) runCatching { val buffer = engine.block.export( block = requireNotNull(engine.scene.get()), mimeType = MimeType.PDF, onPreExport = { scene.getPages().forEach { page -> block.setScopeEnabled(page, key = "layer/visibility", enabled = true) block.setVisible(page, visible = true) } }, ) writeToTempFile(buffer, MimeType.PDF) }.onSuccess { file -> Toast.makeText(editorContext.activity, "Exported to $file!", Toast.LENGTH_SHORT).show() state = state.copy(isLoading = false) // Do something with the file }.onFailure { state = state.copy(isLoading = false, error = it) }}private suspend fun writeToTempFile( byteBuffer: ByteBuffer, mimeType: MimeType = MimeType.PDF,): File = withContext(Dispatchers.IO) { val extension = mimeType.key.split("/").last() File .createTempFile(UUID.randomUUID().toString(), ".$extension") .apply { outputStream().channel.write(byteBuffer) }}onUpload- the callback that is invoked after an asset is added toUploadAssetSourceType. When selecting an asset to upload, a defaultAssetDefinitionobject is constructed based on the selected asset and the callback is invoked. By default, the callback leaves the asset definition unmodified and returns the same object. However, you may want to upload the selected asset to your server before adding it to the scene. Note that the “upload” coroutine job will survive configuration changes and will be cancelled only if the editor is closed or the process is killed when in the background. In this example, we upload theAssetDefinitionto our server and update the instance of the definition.
onUpload = onUpload@{ assetDefinition, _ -> val meta = assetDefinition.meta ?: return@onUpload assetDefinition val sourceUri = meta["uri"]?.toUri() val uploadedUri = sourceUri // todo upload the asset here and return remote uri val newMeta = meta + listOf( "uri" to uploadedUri.toString(), "thumbUri" to uploadedUri.toString(), ) Toast.makeText(editorContext.activity, "onUpload invoked!", Toast.LENGTH_SHORT).show() assetDefinition.copy(meta = newMeta)}onClose- the callback that is invoked afterEditorEvent.OnCloseevent is triggered or when the system back button is clicked and editor cannot handle the event internally. Note that the “close” coroutine job will survive configuration changes and will be cancelled only if the editor is closed or the process is killed when in the background. In this example, we update the state to show a confirmation dialog in theoverlayif there are undo steps available and we close the editor otherwise.
this.onClose = { if (editorContext.engine.editor.canUndo()) { state = state.copy(isCloseConfirmationDialogVisible = true) } else { editorContext.eventHandler.send(EditorEvent.CloseEditor()) }}onError- the callback that is invoked after the editor captures an error. Note that the “error” coroutine job will survive configuration changes and will be cancelled only if the editor is closed or the process is killed when in the background. In this example, we update the state to show an error dialog in theoverlay.
onError = { error -> state = state.copy(error = error)}Full Code#
This complete sample includes an overlay for loading/errors/close confirmation, a dock button for the system camera, and navigation bar buttons for close/export to demonstrate callback behavior in a realistic UI setup.
Here’s the full code for configuring events:
import android.widget.Toastimport androidx.activity.compose.BackHandlerimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Alignmentimport androidx.core.net.toUriimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.coroutineScopeimport kotlinx.coroutines.flow.distinctUntilChangedimport kotlinx.coroutines.flow.mapimport kotlinx.coroutines.launchimport kotlinx.coroutines.withContextimport ly.img.editor.BasicConfigurationBuilderimport ly.img.editor.Editorimport ly.img.editor.core.component.Dockimport ly.img.editor.core.component.EditorComponentimport ly.img.editor.core.component.NavigationBarimport ly.img.editor.core.component.rememberimport ly.img.editor.core.component.rememberCloseEditorimport ly.img.editor.core.component.rememberExportimport ly.img.editor.core.component.rememberSystemCameraimport ly.img.editor.core.configuration.EditorConfigurationimport ly.img.editor.core.configuration.rememberimport ly.img.editor.core.event.EditorEventimport ly.img.editor.core.library.data.AssetSourceTypeimport ly.img.engine.DesignBlockTypeimport ly.img.engine.MimeTypeimport java.io.Fileimport java.io.IOExceptionimport java.nio.ByteBufferimport java.util.UUID
data class State( val isLoading: Boolean = false, val isCloseConfirmationDialogVisible: Boolean = false, val error: Throwable? = null,)
// Add this composable to your NavHost@Composablefun CallbacksEditorSolution( license: String, onClose: (Throwable?) -> Unit,) { var state by remember { mutableStateOf(State()) } Editor( license = license, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember(::BasicConfigurationBuilder) { onCreate = { state = state.copy(isLoading = true) try { val scene = editorContext.engine.scene.create() val page = editorContext.engine.block.create(DesignBlockType.Page) editorContext.engine.block.setWidth(block = page, value = 1080F) editorContext.engine.block.setHeight(block = page, value = 1080F) editorContext.engine.block.appendChild(parent = scene, child = page) editorContext.engine.editor.setSettingEnum(keypath = "touch/pinchAction", value = "Scale") // Add ImageUploads asset source to demonstrate onUpload editorContext.engine.asset.addLocalSource( sourceId = AssetSourceType.ImageUploads.sourceId, supportedMimeTypes = listOf(AssetSourceType.ImageUploads.mimeTypeFilter), ) } finally { state = state.copy(isLoading = false) } } onLoaded = { coroutineScope { launch { editorContext.engine.editor .onStateChanged() .map { editorContext.engine.editor.getEditMode() } .distinctUntilChanged() .collect { Toast.makeText(editorContext.activity, "Edit mode is updated to $it!", Toast.LENGTH_SHORT).show() } } } } onExport = { val engine = editorContext.engine state = state.copy(isLoading = true) runCatching { val buffer = engine.block.export( block = requireNotNull(engine.scene.get()), mimeType = MimeType.PDF, onPreExport = { scene.getPages().forEach { page -> block.setScopeEnabled(page, key = "layer/visibility", enabled = true) block.setVisible(page, visible = true) } }, ) writeToTempFile(buffer, MimeType.PDF) }.onSuccess { file -> Toast.makeText(editorContext.activity, "Exported to $file!", Toast.LENGTH_SHORT).show() state = state.copy(isLoading = false) // Do something with the file }.onFailure { state = state.copy(isLoading = false, error = it) } } onUpload = onUpload@{ assetDefinition, _ -> val meta = assetDefinition.meta ?: return@onUpload assetDefinition val sourceUri = meta["uri"]?.toUri() val uploadedUri = sourceUri // todo upload the asset here and return remote uri val newMeta = meta + listOf( "uri" to uploadedUri.toString(), "thumbUri" to uploadedUri.toString(), ) Toast.makeText(editorContext.activity, "onUpload invoked!", Toast.LENGTH_SHORT).show() assetDefinition.copy(meta = newMeta) } this.onClose = { if (editorContext.engine.editor.canUndo()) { state = state.copy(isCloseConfirmationDialogVisible = true) } else { editorContext.eventHandler.send(EditorEvent.CloseEditor()) } } onError = { error -> state = state.copy(error = error) } overlay = { EditorComponent.remember { decoration = { // Capture system back button tap BackHandler(true) { editorContext.eventHandler.send(EditorEvent.OnClose()) } if (state.isLoading) { Loading() } if (state.isCloseConfirmationDialogVisible) { CloseConfirmationDialog( onDismissRequest = { state = state.copy(isCloseConfirmationDialogVisible = false) }, ) } val error = state.error when { error is IOException -> NoInternetDialog() error != null -> ErrorDialog(throwable = error) } } } } dock = { Dock.remember { listBuilder = { Dock.ListBuilder.remember { add { Dock.Button.rememberSystemCamera() } } } horizontalArrangement = { Arrangement.Center } } } navigationBar = { NavigationBar.remember { listBuilder = { NavigationBar.ListBuilder.remember { aligned(alignment = Alignment.Start) { add { NavigationBar.Button.rememberCloseEditor() } } aligned(alignment = Alignment.End) { add { NavigationBar.Button.rememberExport() } } } } } } } }, onClose = onClose, )}
private suspend fun writeToTempFile( byteBuffer: ByteBuffer, mimeType: MimeType = MimeType.PDF,): File = withContext(Dispatchers.IO) { val extension = mimeType.key.split("/").last() File .createTempFile(UUID.randomUUID().toString(), ".$extension") .apply { outputStream().channel.write(byteBuffer) }}UI Events#
In addition to the callbacks, you can also capture various editor events that may be helpful to track user actions or to do additional actions. All the editor events are available in EditorEvent class.
Also, you can fire your own events and capture them too.
Let’s declare 2 custom events for showing/hiding a loading:
object ShowLoading : EditorEvent
object HideLoading : EditorEventSimilar to our previous guide above, let’s override the onCreate but this time we fire our custom events instead of updating the state:
onCreate = { editorContext.eventHandler.send(ShowLoading) try { val scene = editorContext.engine.scene.create() val page = editorContext.engine.block.create(DesignBlockType.Page) editorContext.engine.block.setWidth(block = page, value = 1080F) editorContext.engine.block.setHeight(block = page, value = 1080F) editorContext.engine.block.appendChild(parent = scene, child = page) } finally { editorContext.eventHandler.send(HideLoading) }}Finally, let’s observe both our custom and some default EditorEvents. The onEvent callback is located in the EditorConfiguration object:
onEvent = { event -> when (event) { is ShowLoading, is HideLoading, is EditorEvent.SetViewMode -> { Toast.makeText(editorContext.activity, "Observing events!", Toast.LENGTH_SHORT).show() } }}Full Code#
Besides onEvent, this sample includes custom ShowLoading/HideLoading events fired from onCreate and observes them together with built-in events (EditorEvent.SetViewMode).
Here’s the full code for UI events:
import android.widget.Toastimport androidx.compose.runtime.Composableimport ly.img.editor.Editorimport ly.img.editor.core.configuration.EditorConfigurationimport ly.img.editor.core.configuration.rememberimport ly.img.editor.core.event.EditorEventimport ly.img.engine.DesignBlockType
object ShowLoading : EditorEvent
object HideLoading : EditorEvent
// Add this composable to your NavHost@Composablefun UiEventsEditorSolution( license: String, onClose: (Throwable?) -> Unit,) { Editor( license = license, // pass null or empty for evaluation mode with watermark configuration = { EditorConfiguration.remember { onCreate = { editorContext.eventHandler.send(ShowLoading) try { val scene = editorContext.engine.scene.create() val page = editorContext.engine.block.create(DesignBlockType.Page) editorContext.engine.block.setWidth(block = page, value = 1080F) editorContext.engine.block.setHeight(block = page, value = 1080F) editorContext.engine.block.appendChild(parent = scene, child = page) } finally { editorContext.eventHandler.send(HideLoading) } } onEvent = { event -> when (event) { is ShowLoading, is HideLoading, is EditorEvent.SetViewMode -> { Toast.makeText(editorContext.activity, "Observing events!", Toast.LENGTH_SHORT).show() } } } } }, onClose = onClose, )}