Overview
Nosmai Cloud lets an application show filters, effects, packaged beauty looks, and backgrounds without including every .nosmai file in the initial application download.
The catalog returns metadata and preview URLs. The protected .nosmai package is downloaded only when it is required. After download, the application applies the returned local file path through the same applyEffect(path) method used for bundled packages.
Request catalog metadata
|
v
Render names and previews
|
v
Download the selected package
|
v
Receive and validate a local path
|
v
Call applyEffect(localPath)
|
v
Confirm selected state from the SDK
Camera frames are not uploaded to list, download, or apply a cloud package.
Requirements
Before requesting cloud filters:
- Initialize the SDK successfully.
- Use a license that includes cloud filters.
- Keep internet access available for catalog requests and downloads.
- Keep enough application storage available for the downloaded package.
- Use the cloud catalog version supported by the installed SDK.
Cloud catalog schema 2.0.0 is the current default. It is separate from Android SDK 3.0.1, iOS SDK 3.0.0, and Flutter package 3.0.6.
An already downloaded and cached package can be applied from its local path without downloading it again. License rules still apply.
Cloud categories
The application should use the platform enum or normalized category where one is available.
| Interface section | Package manifest type | Cloud request value | Flutter enum |
|---|---|---|---|
| Filters | filter | filter | NosmaiCloudFilterType.filter |
| Effects | effect | effects | NosmaiCloudFilterType.effects |
| Beauty | beauty_effect | beauty_effect | NosmaiCloudFilterType.beautyEffect |
| Backgrounds | background | bg | NosmaiCloudFilterType.background |
Cloud request categories and package manifest types are related but not identical. Do not write a cloud request value into a package manifest. The protected package manifest must use filter, effect, beauty_effect, or background.
Recommended interface state
Track cloud state per filter identifier:
notDownloaded downloading downloaded applying selected failed
Also track the catalog request separately:
idle loadingFirstPage showingCachedData loadingNextPage refreshing failed
Recommended behavior:
- Show cached metadata immediately when the platform provides it.
- Refresh the catalog away from the UI thread.
- Keep one current catalog request for each catalog screen or controller.
- Keep one in-flight download for each cloud identifier.
- Ignore repeated taps while that item is downloading or applying.
- Mark an item selected only after apply succeeds.
- Confirm selection from the active-state listener.
- Keep the completed download cached even if its sheet closed during download.
- Do not update a disposed screen when a request finishes.
- Offer retry for temporary network failures.
- Do not delete other categories while processing a scoped or paginated response.
Flutter
List all categories
final nosmai = NosmaiFlutter.instance; final filters = await nosmai.getCloudFilters();
Calling the method without a page requests all available pages for backward compatibility. For a large catalog, request one page at a time.
List one category with pagination
final filters = await nosmai.getCloudFilters( filterType: NosmaiCloudFilterType.background, version: NosmaiCloudFilterVersion.v2, page: 1, limit: 20, fetchAllPages: false, ); final pagination = nosmai.lastPaginationInfo;
Load the next page only when it exists:
if (pagination?.hasNextPage == true) {
final nextPage = await nosmai.getCloudFilters(
filterType: NosmaiCloudFilterType.background,
page: pagination!.currentPage + 1,
limit: pagination.itemsPerPage,
fetchAllPages: false,
);
// Merge by cloudIdentifier to avoid duplicate cells.
}
Do not start a second next-page request while the first one is still running.
Download and apply
Use cloudIdentifier, not the catalog record id, for download and cache operations:
final filter = filters.first;
final result = await nosmai.downloadCloudFilter(
filter.cloudIdentifier,
);
final path = (result['path'] ?? result['localPath']) as String?;
if (path == null || path.isEmpty) {
throw StateError('Cloud filter download returned no local path');
}
final applied = await nosmai.applyEffect(path);
if (!applied) {
// Keep the previous selected state and offer retry.
}
cloudIdentifier preserves compatibility with catalogs that expose separate record and downloadable-package identifiers.
Remove the cached download when the user explicitly requests it:
await nosmai.removeCloudFilter(filter.cloudIdentifier);
Removing a cached file is different from clearing an active render slot. If the item is active, remove its visual state through removeEffect(filter) or the correct narrow clear method as well.
Protect a closing screen
An asynchronous request can finish after its sheet or route is disposed. Check ownership before changing widget state:
if (!context.mounted) return;
Keep download futures in a map keyed by cloudIdentifier. Reuse the existing future for repeated taps and remove it from the map when it completes.
Android
Android exposes cloud operations through NosmaiCloud.
Show cached data without freezing the sheet
NosmaiCloud.cachedList() is an immediate snapshot intended for rendering existing catalog data. It does not perform a network request.
List<NosmaiCloud.Item> cached = NosmaiCloud.cachedList();
List<NosmaiCloud.Item> visible = new ArrayList<>();
for (NosmaiCloud.Item item : cached) {
if ("bg".equals(item.category) ||
"background".equals(item.category)) {
visible.add(item);
}
}
renderCloudItems(visible);
Filter a cached snapshot by the active tab before rendering it. An All snapshot must not be displayed unchanged inside the Filters or Backgrounds tab.
Do not call a network-backed catalog refresh on the main thread. Refresh through an executor and post only the result back to the UI:
private final ExecutorService cloudExecutor =
Executors.newSingleThreadExecutor();
private final Handler mainHandler =
new Handler(Looper.getMainLooper());
private final AtomicLong cloudRequest = new AtomicLong();
private void loadBackgrounds() {
long request = cloudRequest.incrementAndGet();
NosmaiCloud.FilterQuery query =
new NosmaiCloud.FilterQuery();
query.filterType = "bg";
query.page = 1;
query.limit = 20;
query.fetchAllPages = false;
query.cleanupRemoved = false;
cloudExecutor.execute(() -> {
boolean success = NosmaiCloud.fetch(query);
List<NosmaiCloud.Item> items = success
? NosmaiCloud.list()
: Collections.emptyList();
mainHandler.post(() -> {
if (request != cloudRequest.get() || isFinishing()) {
return;
}
if (success) {
renderCloudItems(items);
} else {
showCloudRetry();
}
});
});
}
Increase cloudRequest when the user changes tab, closes the sheet, or starts a new refresh. This prevents an old response from replacing a newer interface state.
Keep cleanupRemoved false for a category-specific or single-page request. Such a response is not the complete server catalog and must not be used to remove cached items from other categories or pages.
Download and apply
NosmaiCloud.download performs the package transfer away from the calling UI flow. Its callbacks must still be marshalled to the main thread before changing views.
NosmaiCloud.download(
item.id,
progress -> mainHandler.post(
() -> updateDownloadProgress(item.id, progress)
),
(filterId, success, localPath, error) -> {
if (!success || localPath == null || localPath.isEmpty()) {
mainHandler.post(() -> showDownloadRetry(filterId));
return;
}
NosmaiEffects.applyEffect(
localPath,
new NosmaiEffects.EffectCallback() {
@Override
public void onSuccess() {
// Confirm final UI from the pipeline listener.
}
@Override
public void onError(String message) {
mainHandler.post(
() -> showApplyRetry(filterId)
);
}
}
);
}
);
Keep a thread-safe set of downloading identifiers. If the set already contains item.id, ignore the repeated tap. Remove the identifier in both success and failure paths.
When the owning screen is destroyed, increment the request generation, remove listeners, and stop the executor if it is owned by that screen. A completed package can remain in the SDK cache for the next screen.
iOS
The iOS effects engine provides asynchronous catalog, progress, download, and apply callbacks.
Request one category
NosmaiCloudFilterRequestOptions *options =
[NosmaiCloudFilterRequestOptions defaultOptions];
options.filterType = @"bg";
options.version = NosmaiCloudFilterVersion2;
options.page = 1;
options.limit = 20;
options.fetchAllPages = NO;
options.cleanupRemovedFilters = NO;
[[NosmaiCore shared].effects
getCloudFiltersWithOptions:options
completion:^(NSArray<NSDictionary *> *filters,
NosmaiCloudFilterPaginationInfo *pagination,
NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (error != nil) {
[self showCloudRetry];
return;
}
[self renderCloudFilters:filters pagination:pagination];
});
}];
Use effects, filter, beauty_effect, or bg for a scoped request. Use nil to request all categories.
Download and apply
NSString *filterId = filter[@"filterId"] ?: filter[@"id"];
[[NosmaiCore shared].effects
downloadCloudFilter:filterId
progress:^(float progress) {
dispatch_async(dispatch_get_main_queue(), ^{
[self updateDownloadProgress:progress filterId:filterId];
});
}
completion:^(BOOL success,
NSString *localPath,
NSError *error) {
if (!success || localPath.length == 0) {
dispatch_async(dispatch_get_main_queue(), ^{
[self showDownloadError:error filterId:filterId];
});
return;
}
[[NosmaiCore shared].effects
applyEffect:localPath
completion:^(BOOL applied, NSError *applyError) {
dispatch_async(dispatch_get_main_queue(), ^{
[self finishApply:applied error:applyError];
});
}];
}];
Keep one in-flight operation per filter identifier. Capture the screen or request owner weakly in production code so a completed callback does not keep a closed sheet alive.
Remove a cached download only when requested:
BOOL removed =
[[NosmaiCore shared].effects removeCloudFilter:filterId];
Applying and clearing rules
The downloaded local path uses the normal package policy:
- A new
filterreplaces the active external color filter. effectandbeauty_effectshare the AR slot and replace each other.- A new
backgroundpackage replaces the previous background package. - Built-in beauty, makeup, reshape, color, and hair controls are mutually exclusive with
effectandbeauty_effect. - A regular external
filtercan remain active with an AR package, background, or built-in beauty. - An AR effect that owns its background can replace existing background content.
Use the active-state listener after apply and removal. Do not decide selected UI only from a tap or downloaded filename.
Error handling
| Failure | Recommended response |
|---|---|
| Cloud feature not licensed | Hide or disable cloud entry points and explain the plan requirement |
| First page fails | Show cached data if available and provide Retry |
| Next page fails | Keep existing items and retry only that page |
| Download interrupted | Keep the item unselected and offer Retry |
| Local path is empty | Treat download as failed and do not call applyEffect |
| Apply fails | Preserve the previous active selection and show a retry action |
| Sheet closes during work | Let safe cache work finish but ignore UI updates for the disposed owner |
| Repeated tap | Reuse or ignore the existing operation for that identifier |
| Cached file was deleted | Download again and refresh metadata |
Do not show backend payloads, full local paths, access tokens, or license keys to the end user.
Test checklist
Before release, test:
- All, Filters, Effects, Beauty, and Background tabs.
- Empty categories and empty search results.
- First page, next page, refresh, and end-of-list behavior.
- Cached data followed by a background refresh.
- Download success, network loss, retry, and low storage.
- Rapid repeated taps on one item.
- Downloads of different items at the same time if the interface allows them.
- Closing and reopening the sheet during a download.
- Applying a downloaded item and reusing it after relaunch.
- Removing the active package without deleting unrelated downloads.
- Removing a cached download and downloading it again.
- Package coexistence and replacement rules.
- Offline launch after the catalog and selected package were cached.
- Slow networks and large catalogs without blocking the camera preview.
Continue with Filters and effects for package slots and Errors and troubleshooting for failure diagnosis.