Keep an open Android Editor asset library in sync after your app changes an external asset catalog.
When an external upload flow, backend job, or sync process changes a custom asset source, update the backing catalog and any cache first. Then call engine.asset.assetSourceContentsChanged(sourceId=_) with that source’s exact ID.
After the engine starts, the Android Editor observes this notification automatically. If an open library category is currently showing a LibraryContent.Grid for that source, or LibraryContent.Sections containing that source, the Editor clears and re-queries the visible content in place. Users do not need to close and reopen the library.
This guide configures a custom AssetSource in the Editor, shows provider-neutral external upload, update, and delete examples, and keeps the engine update flow as an optional integration signal.
When to Use Asset Refresh#
The Editor’s in-place response is automatic after the engine receives a source-change notification. Sending that notification is only necessary for changes that CE.SDK cannot observe directly.
No manual notification is needed for source operations that already notify the engine, such as CE.SDK-managed upload and delete handlers.
Manual notification is needed when another system changes the source data:
- An external upload component stores a new file and updates your app catalog.
- A backend process changes asset labels, tags, thumbnails, or metadata.
- A sync message tells the app that another user added or removed assets.
- A polling job detects that the source’s remote version changed.
Registering a Custom Asset Source#
Start with a stable source ID. This is the same value you pass to assetSourceContentsChanged(sourceId=_) later.
private const val EXTERNAL_IMAGE_SOURCE_ID = "my-external-images"The sample uses a small helper to create image assets with metadata that CE.SDK can read from the source.
private fun externalImageAsset( id: String, label: String, tags: List<String>, uri: String,) = Asset( id = id, context = AssetContext(sourceId = EXTERNAL_IMAGE_SOURCE_ID), label = label, locale = "en", tags = tags, groups = listOf("campaign-assets"), meta = mapOf( "uri" to uri, "thumbUri" to uri, "mimeType" to MimeType.JPEG.key, "kind" to "image", "blockType" to DesignBlockType.Graphic.key, "fillType" to FillType.Image.key, "shapeType" to ShapeType.Rect.key, "width" to "1080", "height" to "1080", ),)Define a source whose findAssets(query=_) reads from your current catalog. The source can implement filtering, paging, and remote loading as needed; this guide keeps the source shape minimal so the refresh calls stay visible.
private class ExternalImageAssetSource( initialAssets: List<Asset>,) : AssetSource(sourceId = EXTERNAL_IMAGE_SOURCE_ID) { var assets = initialAssets
override val supportedMimeTypes = listOf(MimeType.JPEG.key)
override suspend fun getGroups(): List<String>? = listOf("campaign-assets")
override suspend fun findAssets(query: FindAssetsQuery): FindAssetsResult = FindAssetsResult( assets = assets, currentPage = query.page, nextPage = -1, total = assets.size, )}The sample source keeps its catalog in memory so the refresh behavior is easy to verify. In production, findAssets(query=_) usually reads from your database, cache, or app repository.
Create the source instance with the initial assets. The engine-only helper used by the smoke test does this directly:
val externalSource = createExternalImageAssetSource()The engine-only helper registers that source directly before observing update signals:
engine.asset.addSource(externalSource)Configure the Source in the Editor#
Create one source instance, register it in EditorConfiguration.onLoaded, and add the same stable source ID to the asset-library configuration.
@Composablefun RefreshAssetsEditorSolution( license: String, onClose: (Throwable?) -> Unit,) { val externalSource = remember { createExternalImageAssetSource() }
Editor( license = license, configuration = { EditorConfiguration.remember { onLoaded = { val engine = editorContext.engine if (externalSource.sourceId !in engine.asset.findAllSources()) { engine.asset.addSource(externalSource) } }
assetLibrary = { remember { val externalSourceType = AssetSourceType(sourceId = EXTERNAL_IMAGE_SOURCE_ID) val externalImagesSection = LibraryContent.Section( sourceTypes = listOf(externalSourceType), assetType = AssetType.Image, expandContent = LibraryContent.Grid( titleRes = R.string.ly_img_editor_asset_library_section_images, sourceType = externalSourceType, assetType = AssetType.Image, title = "External images", ), ) AssetLibrary.getDefault( images = LibraryCategory.Images.addSection(externalImagesSection), ) } } } }, onClose = onClose, )}onLoaded runs with a started engine, so the custom source can be registered through editorContext.engine. The AssetSourceType used by both the section and its expanded grid has the same EXTERNAL_IMAGE_SOURCE_ID as the source itself and as every later refresh call.
When the Images category is open at its sections view, a change to this source refreshes those visible sections. When the external-images grid is open, the same notification refreshes that grid in place.
Refresh Scope#
The Editor refreshes narrowly to avoid unnecessary source queries:
- Only active, open library categories are eligible for immediate refresh.
- Only the currently displayed grid or sections are checked.
- A grid refreshes only when its source ID exactly matches the emitted ID.
- Sections refresh when at least one displayed section references the emitted source ID.
- Content showing unrelated source IDs and inactive or closed content is not immediately re-queried.
Closed content reads the current source data through its normal query path when the user opens it later. No close-and-reopen workaround is needed for content that is already visible and references the changed source.
Observe the Engine Signal (Optional)#
onAssetSourceUpdated() returns a Flow<String> of updated source IDs. The Editor already collects this flow for its asset library, so applications do not need a second collector to enable UI refresh. Collect it from an app-owned coroutine scope only when other application state also needs to react.
Multiple refreshes for the same source can be coalesced before the next engine update. Treat each event as a signal that the source changed rather than as a receipt for one specific call.
val sourceUpdates = engine.asset.onAssetSourceUpdated()Refreshing After External Uploads#
The next snippets mutate the sample’s in-memory catalog directly. In production, replace those assignments with the upload, database, cache, or sync operation that changes your source data.
When an upload completes outside CE.SDK, make the uploaded file and its catalog entry available, update or invalidate the cache used by findAssets(query=_), and then call assetSourceContentsChanged(sourceId=_) once for the affected source.
externalSource.assets = externalSource.assets + listOf( externalImageAsset( id = "winter-lookbook", label = "Winter Lookbook", tags = listOf("lookbook", "winter"), uri = "https://img.ly/static/ubq_samples/sample_3.jpg", ),)engine.asset.assetSourceContentsChanged(sourceId = externalSource.sourceId)If the source is visible in the Editor, the notification causes an immediate in-place query. Calling it before the external write or cache update finishes can therefore reload stale data.
Refreshing After External Modifications#
Use the same ordering after changing asset metadata such as labels, tags, thumbnails, or dimensions: update the backing catalog and cache first, then notify the exact source ID.
externalSource.assets = externalSource.assets.map { asset -> if (asset.id == "spring-poster") { asset.copy(label = "Spring Launch Poster") } else { asset }}engine.asset.assetSourceContentsChanged(sourceId = externalSource.sourceId)Batch related metadata changes together when possible, then call assetSourceContentsChanged(sourceId=_) once after the batch completes.
Refreshing After External Deletions#
After removing an asset from your external catalog and cache, notify CE.SDK so the visible library query removes the stale entry in place.
externalSource.assets = externalSource.assets.filterNot { asset -> asset.id == "winter-lookbook"}engine.asset.assetSourceContentsChanged(sourceId = externalSource.sourceId)This prevents users from selecting an asset that your backend or app repository no longer serves.
Integration Patterns#
Use the refresh call at the boundary where your app knows the external catalog changed:
| Pattern | When to Refresh |
|---|---|
| Upload callback | After the uploaded file, catalog entry, and cache update are complete |
| Backend sync | After applying the confirmed add, update, or delete to local backing data |
| Polling | After a version check detects a change and the local cache is updated |
| Batch import | Once after all imported assets and metadata have been written |
Avoid refreshing after every item in a large batch. One notification after the batch gives observers a single source update instead of forcing repeated queries.
Troubleshooting#
| Issue | Cause | Solution |
|---|---|---|
| Visible assets do not update | The notification ID differs from the ID in the displayed AssetSourceType |
Use the exact same stable source ID for registration, library content, assets, and notification |
| No immediate library query occurs | The category is closed or inactive, or its currently displayed content references another source | This is expected; only visible active content referencing the changed source refreshes immediately |
| Query still returns old data | The external operation or cache update had not finished when the notification was sent | Update the backing catalog and cache before calling assetSourceContentsChanged(sourceId=_) |
| Source still returns stale assets | findAssets(query=_) serves a cache that was not invalidated |
Invalidate or update that cache before sending the notification |
| Refresh throws | The source is not registered anymore | Confirm the source exists with findAllSources() before refreshing optional sources |
Key Types#
| Type | Purpose |
|---|---|
AssetSource |
Base class for a custom Android asset source |
EditorConfiguration |
Registers the source after engine start and configures Editor behavior |
AssetLibrary |
Defines the categories shown in the Editor asset library |
AssetSourceType |
Connects displayed library content to an exact engine source ID |
LibraryContent |
Defines the grid or sections that can be refreshed in place |
Asset |
Asset result returned from findAssets(query=_) |
FindAssetsQuery |
Search, paging, tag, and group filters for a source query |
FindAssetsResult |
Paged result returned by a source query |
API Reference#
| Method | Description |
|---|---|
EditorConfiguration.onLoaded |
Register a custom source after the Editor engine starts |
engine.asset.addSource(source=_) |
Register a custom asset source |
AssetSource.findAssets(query=_) |
Return the current assets for a query |
AssetSource.getGroups() |
Return the groups exposed by the source |
engine.asset.findAllSources() |
List registered source IDs before optional refresh or cleanup |
engine.asset.onAssetSourceUpdated() |
Optionally observe source IDs emitted by refresh notifications |
engine.asset.assetSourceContentsChanged(sourceId=_) |
Notify CE.SDK after a source’s backing data and cache have changed |
Next Steps#
- Assets — Understand the asset system, custom sources, and source events.
- From a Custom Source — Build a custom source backed by remote data.
- Customize Asset Library — Configure which asset sources appear in the editor UI.