litert-lm

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese
This skill provides step-by-step guidance for building an Android app that runs an open text LLM through the LiteRT-LM Kotlin API (
com.google.ai.edge.litertlm
) on the CPU or the GPU. The model is one
.litertlm
file. Images, audio, tool calling, and NPU backends aren't covered. For vision and audio models with
.tflite
files, suggest the
litert-runtime
skill.
When a LiteRT-LM class or method that this skill uses doesn't compile, you must read the LiteRT-LM Android guide and the LiteRT-LM README before changing the code.
此 skill 提供分步指南,用于构建一个 Android 应用,该应用通过 LiteRT-LM Kotlin API (
com.google.ai.edge.litertlm
) 在 CPU 或 GPU 上运行开放文本 LLM。模型是一个
.litertlm
文件。本 skill 不涵盖图像、音频、工具调用和 NPU 后端。 对于使用
.tflite
文件的视觉和音频模型,建议使用
litert-runtime
skill。
当此 skill 使用的 LiteRT-LM 类或方法无法编译时,在更改代码前,必须 阅读 LiteRT-LM Android 指南 和 LiteRT-LM README。

Prerequisites

前置条件

  • A Kotlin Android project (the Android Studio Empty Activity template is enough). The
    litertlm-android
    0.18.0 AAR declares
    minSdk
    24.
  • The dependency in the app-level
    build.gradle.kts
    file from Google Maven:
    implementation("com.google.ai.edge.litertlm:litertlm-android:0.18.0")
    . Google Maven lists the later releases.
  • For the GPU backend, two
    uses-native-library
    entries inside
    <application>
    in
    AndroidManifest.xml
    . Add the
    INTERNET
    permission if the app downloads the model.
  • The model file lives on the device's file system (hundreds of MB to a few GB), never in
    assets/
    or in the APK.
  • 一个 Kotlin Android 项目(使用 Android Studio 的 Empty Activity 模板即可)。
    litertlm-android
    0.18.0 AAR 声明
    minSdk
    24。
  • 在 app 级别的
    build.gradle.kts
    文件中添加来自 Google Maven 的依赖:
    implementation("com.google.ai.edge.litertlm:litertlm-android:0.18.0")
    。Google Maven 列出了后续版本。
  • 对于 GPU 后端,需要在
    AndroidManifest.xml
    的
    <application>
    中添加两个
    uses-native-library
    条目。如果应用会下载模型,请添加
    INTERNET
    权限。
  • 模型文件存放在设备的文件系统上(数百 MB 到数 GB),绝不能放在
    assets/
    或 APK 中。

Critical mandates

关键要求

  • Dependency first : Before writing code that imports
    com.google.ai.edge.litertlm
    , add the
    litertlm-android
    dependency from Step 1 if the project lacks it. Verify with
    ./gradlew assembleDebug
    .
  • Background initialization :
    Engine(config).initialize()
    loads the weights and blocks for seconds. Call it on a background thread, never on the main thread.
  • Single owner : One single-thread executor owns the
    Engine
    and the
    Conversation
    . Creation, streaming, cancellation, and closing run on it.
  • Close once :
    Engine
    and
    Conversation
    are
    AutoCloseable
    . A second
    close
    on either throws
    IllegalStateException
    . So does
    close
    on an engine whose
    initialize
    threw. Store the engine only after
    initialize
    returns.
  • Model outside the APK : Pass an absolute path to
    EngineConfig
    . Never bundle a
    .litertlm
    file in
    assets/
    .
  • 依赖优先:在编写导入
    com.google.ai.edge.litertlm
    的代码之前,如果项目尚无该依赖,请按步骤 1 添加
    litertlm-android
    依赖。使用
    ./gradlew assembleDebug
    进行验证。
  • 后台初始化:
    Engine(config).initialize()
    会加载权重并阻塞数秒。必须在后台线程调用,绝不能在主线程调用。
  • 单一所有者:使用一个单线程 executor 持有
    Engine
    和
    Conversation
    。创建、流式传输、取消和关闭操作都在该 executor 上运行。
  • 只关闭一次:
    Engine
    和
    Conversation
    均实现了
    AutoCloseable
    。第二次调用任一对象的
    close
    都会抛出
    IllegalStateException
    。如果引擎的
    initialize
    抛出异常,对其调用
    close
    也会抛出异常。仅在
    initialize
    成功返回后存储引擎。
  • 模型置于 APK 之外:向
    EngineConfig
    传入绝对路径。绝不要将
    .litertlm
    文件打包到
    assets/
    中。

Detailed steps

详细步骤

Step 1: Configure Gradle and the manifest

第 1 步:配置 Gradle 和 manifest

Search the project for
libs.versions.toml
,
app/build.gradle.kts
, and
AndroidManifest.xml
before editing build files. Configure
minSdk
and add the LiteRT-LM dependency in
app/build.gradle.kts
:
<br />
android {
    defaultConfig {
        minSdk = 24
    }
}

dependencies {
    implementation("com.google.ai.edge.litertlm:litertlm-android:0.18.0")
}
If the project uses Hilt, check the Dagger version first.
litertlm-android
0.18.0 carries Kotlin 2.4 metadata, which Hilt 2.59.2 can't read. The build then fails in
hiltJavaCompileDebug
with
Provided Metadata instance has version 2.4.0, while maximum supported version is 2.3.0
. Pick one fix:
  • Path A : Upgrade the Hilt version (
    com.google.dagger
    ) to 2.60.1 or later. Ask the user before editing
    gradle/libs.versions.toml
    .
  • Path B : Keep the Hilt version and add a later version of the metadata reader to the annotation processor path in
    app/build.gradle.kts
    :
<br />
dependencies {
    ksp("org.jetbrains.kotlin:kotlin-metadata-jvm:2.4.21")
}
For the GPU backend, add both lines inside
<application>
in
AndroidManifest.xml
. Add the
INTERNET
permission outside
<application>
when the app downloads the model:
<br />
<uses-permission android:name="android.permission.INTERNET"/>

<application>
    <uses-native-library
        android:name="libvndksupport.so"
        android:required="false"/>
    <uses-native-library
        android:name="libOpenCL.so"
        android:required="false"/>
</application>
在编辑构建文件之前,先在项目中搜索
libs.versions.toml
、
app/build.gradle.kts
和
AndroidManifest.xml
。配置
minSdk
,并在
app/build.gradle.kts
中添加 LiteRT-LM 依赖:
<br />
android {
    defaultConfig {
        minSdk = 24
    }
}

dependencies {
    implementation("com.google.ai.edge.litertlm:litertlm-android:0.18.0")
}
如果项目使用 Hilt,请先检查 Dagger 版本。
litertlm-android
0.18.0 包含 Kotlin 2.4 metadata,而 Hilt 2.59.2 无法读取它。随后构建会在
hiltJavaCompileDebug
中失败,并出现以下错误:
Provided Metadata instance has version 2.4.0, while maximum supported version is 2.3.0
。请选择以下一种修复方式:
  • 路径 A:将 Hilt 版本(
    com.google.dagger
    )升级到 2.60.1 或更高版本。编辑
    gradle/libs.versions.toml
    前先询问用户。
  • 路径 B:保持 Hilt 版本不变,并在
    app/build.gradle.kts
    的注解处理器路径中添加更高版本的 metadata 读取器:
<br />
dependencies {
    ksp("org.jetbrains.kotlin:kotlin-metadata-jvm:2.4.21")
}
对于 GPU 后端,在
AndroidManifest.xml
的
<application>
内添加以下两行。 如果应用会下载模型,请在
<application>
外添加
INTERNET
权限:
<br />
<uses-permission android:name="android.permission.INTERNET"/>
<application>
    <uses-native-library
        android:name="libvndksupport.so"
        android:required="false"/>
    <uses-native-library
        android:name="libOpenCL.so"
        android:required="false"/>
</application>

Step 2: Pick a model from litert-community

第 2 步:从 litert-community 选择模型

Choose a
.litertlm
file from litert-community on Hugging Face. Start with
litert-community/gemma-4-E2B-it-litert-lm
(the file
gemma-4-E2B-it.litertlm
, 2.6 GB). For a 0.6 GB model, start with
litert-community/Qwen3-0.6B
(the file
Qwen3-0.6B.litertlm
). Both files run on the CPU and the GPU. A SoC suffix such as
_Google_Tensor_G5
or
.mediatek.mt6993
marks an NPU build, which this skill doesn't cover. The model card names the tested backends and the size.
You must read get a model before you choose a model file other than these two, before you write the in-app download code, and when no
.litertlm
file exists for the model.
从 Hugging Face 上的 litert-community 选择一个
.litertlm
文件。可以先使用
litert-community/gemma-4-E2B-it-litert-lm
(文件
gemma-4-E2B-it.litertlm
, 2.6 GB)。如果需要 0.6 GB 的模型,可以先使用
litert-community/Qwen3-0.6B
(文件
Qwen3-0.6B.litertlm
)。这两个文件都可以在 CPU 和 GPU 上运行。
_Google_Tensor_G5
或
.mediatek.mt6993
等 SoC 后缀表示 NPU 构建,本 skill 不涵盖这种情况。 model card 会列出经过测试的 backend 和模型大小。
除上述两个模型外,在选择其他模型文件之前、编写应用内下载代码之前, 以及不存在对应
.litertlm
文件时,必须阅读 获取模型。

Step 3: Put the model file on the device

第 3 步:将模型文件放到设备上

Pick one path:
  • Path A (local testing with
    adb
    )
    : Run the following commands only when all three are true:
    adb devices
    lists a device, the debug build of the app is installed on it, and the user supplied the local
    .litertlm
    path. If any of them is false, don't run the commands. Report them to the user as a post-implementation step instead. Replace
    <path>
    with the user's local path and
    <package>
    with the app's application ID:
    1. adb push <path> /data/local/tmp/model.litertlm
    2. adb shell run-as <package> cp /data/local/tmp/model.litertlm files/
  • Path B (in-app download) : Write the download code in the app. Use the app's HTTP client to write into
    filesDir
    , or
    DownloadManager
    to write into
    getExternalFilesDir(null)
    .
    DownloadManager
    can't write to internal storage. Show the download progress and check the file size against the model card before loading. Never download a model file in the agent's own session.
  • Path C (Play for On-Device AI) : Write the code that resolves the path of a fast-follow or on-demand AI pack with the AI Delivery library (
    com.google.android.play:ai-delivery
    ):
    AiPackManagerFactory.getInstance(context).getPackLocation(name)?.assetsPath()
    . The call returns
    null
    until the pack has finished downloading, so show a download state in that case. Creating the AI pack module and uploading the app to Play Console are post-implementation steps for the user.
On every path, pass the absolute path of the file to the engine.
选择一种路径:
  • 路径 A(使用
    adb
    进行本地测试)
    :仅当以下三个条件全部成立时,才运行以下命令:
    adb devices
    列出了设备、应用的 debug build 已安装到设备上,并且用户提供了本地
    .litertlm
    路径。如果任一条件不成立,则不要运行这些命令;应将其作为实现后步骤告知用户。将
    <path>
    替换为用户的本地路径,将
    <package>
    替换为应用的 application ID:
    1. adb push <path> /data/local/tmp/model.litertlm
    2. adb shell run-as <package> cp /data/local/tmp/model.litertlm files/
  • 路径 B(应用内下载):在应用中编写下载代码。使用应用的 HTTP 客户端写入
    filesDir
    ,或使用
    DownloadManager
    写入
    getExternalFilesDir(null)
    。
    DownloadManager
    无法写入内部存储。显示下载进度,并在加载前根据 model card 检查文件大小。绝不要在 Agent 自身的会话中下载模型文件。
  • 路径 C(Play for On-Device AI):编写代码,使用 AI Delivery library(
    com.google.android.play:ai-delivery
    )解析 fast-follow 或按需 AI pack 的路径:
    AiPackManagerFactory.getInstance(context).getPackLocation(name)?.assetsPath()
    。 在 pack 下载完成之前,该调用会返回
    null
    ,因此此时应显示下载状态。创建 AI pack module 和将应用上传到 Play Console 是用户需要完成的后续步骤。
无论采用哪种路径,都要向引擎传入文件的绝对路径。

Step 4: Initialize the engine off the main thread

第 4 步:在主线程之外初始化引擎

Create the engine with
EngineConfig(modelPath = ..., backend = ..., cacheDir = ...)
and call
initialize
on a background thread:
  • Backend.CPU()
    is the default of
    EngineConfig
    and needs nothing from the device.
  • Backend.GPU()
    needs the two manifest lines from Step 1. Without them, the engine initializes, and the first reply ends in the error
    Can not find OpenCL library on this device
    .
  • cacheDir
    speeds up the second load.
使用
EngineConfig(modelPath = ..., backend = ..., cacheDir = ...)
创建引擎,并在线程中调用
initialize
:
  • Backend.CPU()
    是
    EngineConfig
    的默认 backend,不需要对设备进行任何额外配置。
  • Backend.GPU()
    需要第 1 步中的两个 manifest 配置项。如果没有这些配置,引擎仍会初始化,但首次回复最终会报错
    Can not find OpenCL library on this device
    。
  • cacheDir
    可以加快第二次加载速度。

Step 5: Implement the conversation ViewModel

第 5 步:实现会话 ViewModel

Drive the engine and the conversation from a single-thread executor inside an
AndroidViewModel
, stream the reply, and expose immutable state to the UI:
<br />
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import com.google.ai.edge.litertlm.Backend
import com.google.ai.edge.litertlm.Contents
import com.google.ai.edge.litertlm.Conversation
import com.google.ai.edge.litertlm.ConversationConfig
import com.google.ai.edge.litertlm.Engine
import com.google.ai.edge.litertlm.EngineConfig
import java.util.concurrent.Executors
import kotlin.coroutines.cancellation.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

data class ChatState(
    val ready: Boolean = false,
    val busy: Boolean = false,
    val reply: String = "",
    val error: String? = null
)

class ChatViewModel(app: Application) : AndroidViewModel(app) {
    private val executor = Executors.newSingleThreadExecutor()
    private val scope = CoroutineScope(
        SupervisorJob() + executor.asCoroutineDispatcher()
    )
    private val mutex = Mutex()
    private var engine: Engine? = null
    private var conversation: Conversation? = null
    private val _state = MutableStateFlow(ChatState())
    val state: StateFlow<ChatState> = _state.asStateFlow()

    fun load(modelPath: String, backend: Backend = Backend.CPU()) {
        scope.launch {
            mutex.withLock {
                if (engine?.engineConfig?.modelPath == modelPath) return@launch
                release()
                try {
                    val config = EngineConfig(
                        modelPath = modelPath,
                        backend = backend,
                        cacheDir = getApplication<Application>().cacheDir.path
                    )
                    engine = Engine(config).also { it.initialize() }
                    conversation = checkNotNull(engine).createConversation(
                        ConversationConfig(
                            systemInstruction =
                                Contents.of("You are a helpful assistant.")
                        )
                    )
                    _state.value = ChatState(ready = true)
                } catch (e: Exception) {
                    release()
                    _state.value = ChatState(error = e.message)
                }
            }
        }
    }

    fun send(text: String) {
        scope.launch {
            if (!_state.value.ready || _state.value.busy) return@launch
            _state.update { it.copy(busy = true, reply = "", error = null) }
            try {
                val replies = checkNotNull(conversation).sendMessageAsync(text)
                replies.collect { message ->
                    _state.update { it.copy(reply = it.reply + message) }
                }
            } catch (e: CancellationException) {
                throw e
            } catch (e: Exception) {
                _state.update { it.copy(error = e.message) }
            } finally {
                _state.update { it.copy(busy = false) }
            }
        }
    }

    private suspend fun release() {
        _state.update { it.copy(ready = false) }
        while (_state.value.busy) {
            conversation?.cancelProcess()
            delay(100)
        }
        conversation?.close()
        conversation = null
        engine?.close()
        engine = null
        _state.value = ChatState()
    }

    override fun onCleared() {
        scope.launch {
            mutex.withLock { release() }
            executor.shutdown()
        }.invokeOnCompletion { scope.cancel() }
    }
}
sendMessageAsync(text)
returns a
Flow<Message>
of chunks, and
toString
gives a chunk's text.
sendMessage(text)
blocks and returns the whole reply. The conversation keeps its history, so the next
send
continues the chat.
busy
keeps the button disabled until the flow completes.
To set the sampling, pass
SamplerConfig(topK = 10, topP = 0.95, temperature = 0.8)
in
ConversationConfig
. All three parameters are required, and
topP
and
temperature
are
Double
.
load
with the model path that is already loaded does nothing, whatever backend it asks for. A second call after a rotation keeps the loaded model, also after the app has fallen back to another backend.
send
runs only when the state is
ready
and not
busy
, the same test as the button. A tap that lands while
release
stops a reply does nothing. A tap queued behind a
load
reaches the model that
load
loaded when no reply was streaming.
在
AndroidViewModel
内使用单线程 executor 驱动引擎和会话,流式接收回复,并向 UI 暴露不可变状态:
<br />
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import com.google.ai.edge.litertlm.Backend
import com.google.ai.edge.litertlm.Contents
import com.google.ai.edge.litertlm.Conversation
import com.google.ai.edge.litertlm.ConversationConfig
import com.google.ai.edge.litertlm.Engine
import com.google.ai.edge.litertlm.EngineConfig
import java.util.concurrent.Executors
import kotlin.coroutines.cancellation.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

data class ChatState(
    val ready: Boolean = false,
    val busy: Boolean = false,
    val reply: String = "",
    val error: String? = null
)

class ChatViewModel(app: Application) : AndroidViewModel(app) {
    private val executor = Executors.newSingleThreadExecutor()
    private val scope = CoroutineScope(
        SupervisorJob() + executor.asCoroutineDispatcher()
    )
    private val mutex = Mutex()
    private var engine: Engine? = null
    private var conversation: Conversation? = null
    private val _state = MutableStateFlow(ChatState())
    val state: StateFlow<ChatState> = _state.asStateFlow()

    fun load(modelPath: String, backend: Backend = Backend.CPU()) {
        scope.launch {
            mutex.withLock {
                if (engine?.engineConfig?.modelPath == modelPath) return@launch
                release()
                try {
                    val config = EngineConfig(
                        modelPath = modelPath,
                        backend = backend,
                        cacheDir = getApplication<Application>().cacheDir.path
                    )
                    engine = Engine(config).also { it.initialize() }
                    conversation = checkNotNull(engine).createConversation(
                        ConversationConfig(
                            systemInstruction =
                                Contents.of("You are a helpful assistant.")
                        )
                    )
                    _state.value = ChatState(ready = true)
                } catch (e: Exception) {
                    release()
                    _state.value = ChatState(error = e.message)
                }
            }
        }
    }

    fun send(text: String) {
        scope.launch {
            if (!_state.value.ready || _state.value.busy) return@launch
            _state.update { it.copy(busy = true, reply = "", error = null) }
            try {
                val replies = checkNotNull(conversation).sendMessageAsync(text)
                replies.collect { message ->
                    _state.update { it.copy(reply = it.reply + message) }
                }
            } catch (e: CancellationException) {
                throw e
            } catch (e: Exception) {
                _state.update { it.copy(error = e.message) }
            } finally {
                _state.update { it.copy(busy = false) }
            }
        }
    }

    private suspend fun release() {
        _state.update { it.copy(ready = false) }
        while (_state.value.busy) {
            conversation?.cancelProcess()
            delay(100)
        }
        conversation?.close()
        conversation = null
        engine?.close()
        engine = null
        _state.value = ChatState()
    }

    override fun onCleared() {
        scope.launch {
            mutex.withLock { release() }
            executor.shutdown()
        }.invokeOnCompletion { scope.cancel() }
    }
}
sendMessageAsync(text)
返回由文本分块组成的
Flow<Message>
,对分块调用
toString
即可获得其文本。
sendMessage(text)
会阻塞,并返回完整回复。 会话会保留历史记录,因此下一次
send
将继续当前对话。
busy
会在 Flow 完成前保持按钮禁用。
要设置采样参数,请在
ConversationConfig
中传入
SamplerConfig(topK = 10, topP = 0.95, temperature = 0.8)
。三个参数均为必填项, 其中
topP
和
temperature
的类型为
Double
。
使用已加载的模型路径调用
load
时不会执行任何操作,无论请求的是哪个 backend。屏幕旋转后的第二次调用会保留已加载的模型,应用回退到其他 backend 后也是如此。
send
仅在状态为
ready
且非
busy
时运行,与按钮的启用条件相同。
release
停止回复期间发生的点击不会执行任何操作。 当没有回复正在流式传输时,排在
load
之后的点击会访问
load
所加载的模型。

Step 6: Wire the Compose screen

第 6 步:连接 Compose 屏幕

viewModel
and
collectAsStateWithLifecycle
come from
androidx.lifecycle:lifecycle-viewmodel-compose
and
androidx.lifecycle:lifecycle-runtime-compose
(2.10.0):
<br />
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.ai.edge.litertlm.Backend

@Composable
fun ChatScreen(modelPath: String, viewModel: ChatViewModel = viewModel()) {
    val state by viewModel.state.collectAsStateWithLifecycle()
    var input by remember { mutableStateOf("") }
    LaunchedEffect(modelPath) { viewModel.load(modelPath, Backend.GPU()) }
    Column {
        Text(
            state.reply,
            Modifier.weight(1f).verticalScroll(rememberScrollState())
        )
        state.error?.let { Text(it) }
        TextField(value = input, onValueChange = { input = it })
        Button(
            onClick = { viewModel.send(input); input = "" },
            enabled = state.ready && !state.busy
        ) {
            Text("Send")
        }
    }
}
viewModel
和
collectAsStateWithLifecycle
分别来自
androidx.lifecycle:lifecycle-viewmodel-compose
和
androidx.lifecycle:lifecycle-runtime-compose
(2.10.0):
<br />
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.ai.edge.litertlm.Backend

@Composable
fun ChatScreen(modelPath: String, viewModel: ChatViewModel = viewModel()) {
    val state by viewModel.state.collectAsStateWithLifecycle()
    var input by remember { mutableStateOf("") }
    LaunchedEffect(modelPath) { viewModel.load(modelPath, Backend.GPU()) }
    Column {
        Text(
            state.reply,
            Modifier.weight(1f).verticalScroll(rememberScrollState())
        )
        state.error?.let { Text(it) }
        TextField(value = input, onValueChange = { input = it })
        Button(
            onClick = { viewModel.send(input); input = "" },
            enabled = state.ready && !state.busy
        ) {
            Text("Send")
        }
    }
}

Step 7: Manage the engine lifecycle

第 7 步:管理引擎生命周期

  • Keep the engine in the
    ViewModel
    for as long as its screen lives. When several screens share the engine, keep it in an application-scoped holder.
  • The
    ViewModel
    keeps its own thread and scope.
    viewModelScope
    is cancelled before
    onCleared
    runs, so a close launched there wouldn't run.
    onCleared
    cancels
    scope
    once the close is done.
  • release
    turns
    ready
    off and calls
    cancelProcess
    until the streaming reply's flow has ended. The flow ends at the latest when the reply ends on its own.
    release
    then closes the conversation and the engine, each once.
  • onCleared
    and a
    load
    that replaces the model both call
    release
    under the
    Mutex
    , one at a time, because
    release
    suspends while a reply stops.
  • To start a fresh chat, close the conversation and create a new one with
    createConversation
    . Keep the engine loaded.
  • 只要屏幕仍然存在,就在
    ViewModel
    中保留引擎。当多个屏幕共享引擎时,将其保存在 application 作用域的 holder 中。
  • ViewModel
    拥有自己的线程和 scope。
    viewModelScope
    会在
    onCleared
    运行之前被取消,因此在那里启动的 close 操作不会执行。
    onCleared
    会在 close 完成后取消
    scope
    。
  • release
    会关闭
    ready
    状态,并调用
    cancelProcess
    ,直到流式回复的 Flow 结束。Flow 最迟会在回复自行结束时结束。随后,
    release
    会分别关闭 conversation 和 engine,且每个对象只关闭一次。
  • onCleared
    和替换模型的
    load
    都会在
    Mutex
    下调用
    release
    ,并且一次只执行一个,因为
    release
    在停止回复期间会挂起。
  • 要开始一段全新对话,请关闭 conversation,并使用
    createConversation
    创建一个新 conversation,同时保持引擎处于加载状态。

Post-implementation steps for the user

面向用户的实现后步骤

Report the steps that apply to the user after the code compiles:
  • When Path A in Step 3 didn't run, copy the model to the device with the two
    adb
    commands from Path A.
  • When the app uses Path C in Step 3, create the AI pack module that holds the
    .litertlm
    file and upload the app to Play Console.
  • When no
    .litertlm
    file exists for the model, convert the model on a workstation. This skill doesn't convert models.
  • When the user asks for a complete reference app, point them to Google AI Edge Gallery.
代码编译完成后,向用户报告适用于其情况的步骤:
  • 如果第 3 步的路径 A 未执行,请使用路径 A 中的两条
    adb
    命令将模型复制到设备。
  • 如果应用使用第 3 步的路径 C,请创建用于保存
    .litertlm
    文件的 AI pack module,并将应用上传到 Play Console。
  • 如果模型不存在对应的
    .litertlm
    文件,请在工作站上进行转换。本 skill 不负责转换模型。
  • 如果用户需要完整的参考应用,请引导其访问 Google AI Edge Gallery。

Troubleshooting

故障排除

SymptomCauseFix
First reply is slow.
initialize
ran on first use.
Load at app start and set
cacheDir
.
First GPU reply ends in
Can not find OpenCL library on this device
.
The manifest lacks the
uses-native-library
entries.
Add both entries from Step 1 inside
<application>
.
close
throws
IllegalStateException
.
The engine or conversation was closed twice, or
initialize
threw.
Store the engine only after
initialize
returns, and set each field to
null
after
close
.
Build fails with
[Hilt] Provided Metadata instance has version 2.4.0
.
Hilt 2.59.2 can't read the Kotlin 2.4 metadata in
litertlm-android
0.18.0.
Follow Path A or Path B in Step 1.
症状原因修复方案
首次回复较慢。
initialize
在首次使用时运行。
应用启动时加载模型并设置
cacheDir
。
首次 GPU 回复以
Can not find OpenCL library on this device
结束。
manifest 缺少
uses-native-library
条目。
在
<application>
内添加第 1 步中的两个条目。
close
抛出
IllegalStateException
。
engine 或 conversation 被关闭了两次,或者
initialize
抛出了异常。
仅在
initialize
成功返回后存储 engine,并在
close
后将相应字段设为
null
。
构建因
[Hilt] Provided Metadata instance has version 2.4.0
而失败。
Hilt 2.59.2 无法读取
litertlm-android
0.18.0 中的 Kotlin 2.4 metadata。
按第 1 步的路径 A 或路径 B 处理。

Checklist

检查清单

  • [ ] Is
    minSdk
    set to
    24
    or higher and is
    com.google.ai.edge.litertlm:litertlm-android:0.18.0
    added in
    app/build.gradle.kts
    ?
  • [ ] For the GPU backend, does
    AndroidManifest.xml
    declare
    libvndksupport.so
    and
    libOpenCL.so
    with
    android:required="false"
    inside
    <application>
    ?
  • [ ] Is the
    .litertlm
    file loaded from an absolute path on the device's file system, never from
    assets/
    ?
  • [ ] Do
    initialize
    ,
    sendMessageAsync
    ,
    cancelProcess
    , and
    close
    all run on the same single-thread background executor?
  • [ ] Is the conversation closed before the engine, each exactly once?
  • [ ] Does the project compile with
    ./gradlew assembleDebug
    ?
  • [ ] 是否已将
    minSdk
    设置为
    24
    或更高,并在
    app/build.gradle.kts
    中添加了
    com.google.ai.edge.litertlm:litertlm-android:0.18.0
    ?
  • [ ] 对于 GPU backend,
    AndroidManifest.xml
    是否在
    <application>
    内声明了
    libvndksupport.so
    和
    libOpenCL.so
    ,并设置
    android:required="false"
    ?
  • [ ] 是否通过设备文件系统中的绝对路径加载
    .litertlm
    文件,而没有从
    assets/
    加载?
  • [ ]
    initialize
    、
    sendMessageAsync
    、
    cancelProcess
    和
    close
    是否都在同一个单线程后台 executor 上运行?
  • [ ] 是否先关闭 conversation、再关闭 engine,并且每个对象恰好关闭一次?
  • [ ] 项目能否通过
    ./gradlew assembleDebug
    编译?

Antipatterns

反模式

  • Never call
    Engine.initialize
    on the Android main thread.
  • Never place a
    .litertlm
    file in
    assets/
    or ship it inside the APK.
  • Never call
    close
    on an engine whose
    initialize
    threw.
  • Never close the engine while a reply is streaming; call
    cancelProcess
    until the flow ends first.
  • Never close
    Engine
    or
    Conversation
    from
    viewModelScope
    in
    onCleared
    ; that scope is already cancelled.
  • 绝不要在 Android 主线程调用
    Engine.initialize
    。
  • 绝不要将
    .litertlm
    文件放在
    assets/
    中或打包到 APK 内。
  • 绝不要对
    initialize
    抛出异常的 engine 调用
    close
    。
  • 绝不要在回复仍在流式传输时关闭 engine;应先调用
    cancelProcess
    ,直到 Flow 结束。
  • 绝不要在
    onCleared
    中通过
    viewModelScope
    关闭
    Engine
    或
    Conversation
    ;该 scope 已经被取消。