Foundry Local runs inference on the Android device, so application lifecycle, coroutine ownership, memory, and storage directly affect reliability.
Call suspend functions from a scope that has a clear owner, such as viewModelScope:
viewModelScope.launch(Dispatchers.IO) {
try {
val response = chatClient.completeChat(request)
withContext(Dispatchers.Main) {
render(response.message?.content.orEmpty())
}
} catch (cancelled: CancellationException) {
throw cancelled
} catch (error: FoundryLocalException) {
withContext(Dispatchers.Main) {
showError(error.message ?: "Inference failed")
}
}
}Avoid unmanaged application-wide scopes for UI work. Keep a process-level scope only for an operation that intentionally outlives an Activity, and expose its state back to the UI.
Downloads and streaming inference support coroutine cancellation. Always rethrow
CancellationException before handling other failures:
suspend fun collectResponse(
chatClient: ChatClient,
request: ChatCompletionRequest
) {
try {
chatClient.completeChatStreaming(request).collect(::appendChunk)
} catch (cancelled: CancellationException) {
throw cancelled
} catch (error: FoundryLocalException) {
report(error)
}
}Store the Job when the user needs a Cancel or Stop action.
Use the same sequence in IPC and embedded modes:
- Discover a model with
catalog.listModels(). - Get its handle with
catalog.getModel(info.alias). - Download it if
isCached()is false. - Load it if
isLoaded()is false. - Create and reuse the appropriate client.
- Cancel active work before unloading.
- Remove cached files only after unloading.
suspend fun prepareChatModel(catalog: Catalog, alias: String): Pair<Model, ChatClient> {
val model = catalog.getModel(alias)
if (!model.isCached()) {
model.download()
}
if (!model.isLoaded()) {
model.load()
}
return model to model.createChatClient()
}Do not repeatedly create clients for the same loaded model. Reuse the client until the model is unloaded or the IPC connection is replaced.
Treat the model alias as application configuration and verify it against the current catalog:
suspend fun validateModel(catalog: Catalog, modelAlias: String): ModelInfo {
return catalog.getModelInfo(modelAlias)
}Handle a missing or incompatible configured model explicitly.
IPC mode can lose its service process independently of your app. Register onDisconnected, move
the UI into a disconnected state, and reconnect from a coroutine:
suspend fun createIpcManager(context: Context): FoundryLocalManager {
return FoundryLocalManager.create(
context,
Configuration(appName = "MyApp"),
onDisconnected = {
mainHandler.post { showReconnectAction() }
}
)
}After manager.reconnect():
- Call
manager.getCatalog()again. - Reacquire the model by alias.
- Recheck cached and loaded state.
- Recreate chat or audio clients.
Do not continue using handles acquired before the disconnection.
Embedded mode has no service connection. Its failures occur in your app process and cannot be
recovered through reconnect().
Run model operations away from the main thread. Progress callbacks and the IPC disconnection
callback are not guaranteed to execute on the main thread, so switch to Dispatchers.Main before
updating views or Compose state.
Avoid logging prompts, responses, transcripts, or raw audio. Log operation names, aliases, durations, counts, and payload sizes instead.
Loaded models consume device memory until unloaded:
- load only the models needed for the current experience;
- cancel inference before unloading its model;
- unload models when their owning feature is finished;
- avoid loading multiple large models unless the device has been tested for that workload; and
- in embedded mode, test alongside the rest of the app's memory-intensive features.
Do not use a fixed free-memory threshold copied from another app. Android memory pressure varies by device, process state, model, and workload.
Send only history needed for the current response:
val request = ChatCompletionRequest(
messages = conversation.takeLast(maxMessages)
)Context limits differ by model. Follow the selected model's documentation rather than using a universal token limit. Preserve system instructions when trimming and avoid splitting a logical user/assistant turn.
Start with model defaults. Set sampling controls only when the selected model documents them and your application needs them. Validate empty input before starting inference.
Package either the IPC release AAR or the embedded release AAR. Do not include both unless the specific release documents that combination as supported.
When switching modes, retest:
- initialization and cleanup;
- model cache location and disk use;
- memory pressure and process recovery;
- IPC installation and reconnection, when applicable; and
- final APK or App Bundle packaging.
Cancel active jobs before releasing their dependencies:
suspend fun releaseResources(
manager: FoundryLocalManager,
model: Model,
audioSession: AudioStreamSession?,
streamingJob: Job?
) {
streamingJob?.cancelAndJoin()
audioSession?.stop()
if (model.isLoaded()) {
model.unload()
}
manager.close()
}Run suspend cleanup from an appropriate coroutine. Do not issue new operations after close().
Inference executes locally, but catalog access, model downloads, service-app installation or updates, and telemetry can use the network. Do not promise that the application never communicates externally unless the complete release configuration and application behavior have been verified.
Avoid putting secrets or user content in additionalSettings, logs, analytics, or exception
messages.