App Functions 可以把 App 里原本藏在页面背后的能力,变成一组可以被系统或 Agent 发现、理解、执行的函数。
这件事的重点不在“AI”两个字,而在接口形态变了。过去 Android App 的主入口是 Activity、Deep Link、Intent。现在官方开始给另一套入口:函数调用。
如果一个记事 App 暴露了 createNote(),一个提醒 App 暴露了 scheduleReminder(),那系统或者 Agent 理论上就可以不经过层层 UI 点击,直接完成跨 App 编排。
这也是它比普通 Android 题更高阶的地方。它讨论的不是界面,不是状态管理,不是某个组件怎么用,而是 App 的能力建模、函数元数据、编译期生成、服务暴露、跨进程执行、可观测发现、运行时开关和测试。
先看官方现在的依赖。按 2026-04-11 这个时间点,AndroidX 最新是 1.0.0-alpha08,发布时间是 2026-03-11。
dependencies {
implementation("androidx.appfunctions:appfunctions:1.0.0-alpha08")
implementation("androidx.appfunctions:appfunctions-service:1.0.0-alpha08")
ksp("androidx.appfunctions:appfunctions-compiler:1.0.0-alpha08")
}
这里已经能看出它的结构了。appfunctions 是核心 API,appfunctions-service 负责服务侧暴露,appfunctions-compiler 通过 KSP 在编译期生成元数据和接线代码。也就是说,这不是一个“运行时随手调一下”的库,它本身就是一套声明式能力暴露机制。
一个最小函数
最基础的入口是 @AppFunction。
import androidx.appfunctions.service.AppFunction
class NoteFunctions(
private val noteRepository: NoteRepository
) {
/**
* Create a new note.
*
* @param title note title
* @param content note content
* @return created note
*/
@AppFunction(isDescribedByKDoc = true)
suspend fun createNote(
title: String,
content: String
): NoteDto {
require(title.isNotBlank()) { "title is empty" }
require(content.isNotBlank()) { "content is empty" }
val note = noteRepository.create(title, content)
return NoteDto(
id = note.id,
title = note.title,
content = note.content
)
}
}
这个写法里有两个点很关键。
第一,@AppFunction 不只是个标记。官方文档明确写了,编译器会为这些函数生成 XML 元数据,并提供把它们暴露给 AppFunctionService 的基础设施。
第二,isDescribedByKDoc = true 很值钱。因为它不是拿来给人看注释这么简单,而是会把 KDoc 里的函数描述、参数描述、返回值描述,转成函数元数据。对 Agent 来说,这些描述不是装饰,而是理解能力边界的一部分。
返回类型通常应该保持干净、可序列化、不要夹带太多平台对象。比如可以这么定义:
import androidx.appfunctions.AppFunctionSerializable
@AppFunctionSerializable
data class NoteDto(
val id: Long,
val title: String,
val content: String
)
参数对象也一样,尽量做成明确的数据结构,不要一股脑塞 Map<String, Any>。
@AppFunctionSerializable
data class CreateNoteParams(
val title: String,
val content: String
)
然后把函数签名收敛一下:
class NoteFunctions(
private val noteRepository: NoteRepository
) {
@AppFunction(isDescribedByKDoc = true)
suspend fun createNote(params: CreateNoteParams): NoteDto {
require(params.title.isNotBlank()) { "title is empty" }
require(params.content.isNotBlank()) { "content is empty" }
val note = noteRepository.create(params.title, params.content)
return NoteDto(note.id, note.title, note.content)
}
}
这种写法的好处是,函数边界会更稳定。后面要扩参数、做 schema、接 Agent,都比一堆散参数更舒服。
不只是函数,还要有服务
函数写完,不代表系统就能调用。你还要把它挂到 AppFunctionService 上。
官方文档要求在 manifest 里加服务:
<service
android:name=".NoteAppFunctionService"
android:permission="android.permission.BIND_APP_FUNCTION_SERVICE">
<intent-filter>
<action android:name="android.app.appfunctions.AppFunctionService" />
</intent-filter>
</service>
服务本身长这样:
import androidx.appfunctions.AppFunctionException
import androidx.appfunctions.ExecuteAppFunctionRequest
import androidx.appfunctions.ExecuteAppFunctionResponse
import androidx.appfunctions.service.AppFunctionConfiguration
import androidx.appfunctions.AppFunctionService
class NoteAppFunctionService : AppFunctionService(),
AppFunctionConfiguration.Provider {
override val appFunctionConfiguration: AppFunctionConfiguration
get() = AppFunctionConfiguration.Builder()
.addEnclosingClassFactory(NoteFunctions::class) {
NoteFunctions(DefaultNoteRepository())
}
.build()
override fun executeFunction(
request: ExecuteAppFunctionRequest
): ExecuteAppFunctionResponse {
throw AppFunctionException(
errorCode = 0,
errorMessage = "Use compiler generated dispatch path"
)
}
}
这里最值得注意的是 addEnclosingClassFactory()。官方文档专门提到,如果 @AppFunction 所在类不是无参构造,或者你需要注入依赖,就通过这个工厂接进去。
这件事挺重要,因为它意味着 App Functions 不要求你把业务逻辑写成一堆静态函数。你完全可以保留正常的 repository / use case 结构,只是多暴露一个面向系统的函数入口。
建模比调用更重要
很多人看到这种能力,第一反应是“怎么执行函数”。但真正难的通常不是执行,而是建模。
比如下面这两个函数,技术上都能工作:
@AppFunction
suspend fun createNote(title: String, content: String): NoteDto
@AppFunction
suspend fun createNote(params: CreateNoteParams): NoteDto
第二种通常更适合往长期演进。因为一旦后面要补字段,比如 tags、folderId、pinned、source,参数对象能稳住接口形态,也更适合和 schema 对齐。
如果函数准备长期暴露给 Agent,用 schema 来描述会更清楚。alpha08 里新加了 @AppFunctionSchemaDefinition,官方给的是这种方向:
import androidx.appfunctions.AppFunctionContext
import androidx.appfunctions.AppFunctionSchemaDefinition
@AppFunctionSchemaDefinition(
name = "createNote",
version = 1,
category = "Notes"
)
interface CreateNoteSchema {
suspend fun createNote(
appFunctionContext: AppFunctionContext,
params: CreateNoteParams
): NoteDto
}
这个东西的意义不是“语法更好看”,而是把函数能力正式升级成可检索、可共享、可版本化的 schema。官方文档里直接写了,Agent 可以通过 AppFunctionManager.observeAppFunctions() 拿到这些 schema 元数据。再往前走一点,这就不是单个 App 的私有协议了,而是一种能力协定。
发现函数,不靠猜
App Functions 不是直接拿字符串硬调。先发现,再执行。
alpha08 里的 AppFunctionManager 已经支持观察可用函数元数据:
import androidx.appfunctions.AppFunctionManager
import androidx.appfunctions.AppFunctionSearchSpec
import kotlinx.coroutines.flow.first
suspend fun discoverNoteFunctions(context: Context) {
val manager = AppFunctionManager.getInstance(context) ?: return
val packages = manager.observeAppFunctions(
AppFunctionSearchSpec(
packageNames = listOf("com.example.notes"),
schemaName = "createNote"
)
).first()
val packageMetadata = packages.singleOrNull() ?: return
val functionMetadata = packageMetadata.appFunctions.firstOrNull() ?: return
println(functionMetadata.id)
println(functionMetadata.description)
}
这个模型和传统 Android 很不一样。以前跨 App 能力更像“我知道你有个 deep link,所以我跳过去”。现在更像“我先查你暴露了什么能力,再决定调哪个函数”。
这也是它高级的地方。入口从“页面地址”转成了“能力描述”。
执行函数
真正执行时,核心对象是 ExecuteAppFunctionRequest。官方参考页对完整 happy path 示例还比较散,下面这段我用的是接近实际 API 形态的示意代码,重点看调用模型:
import androidx.appfunctions.AppFunctionData
import androidx.appfunctions.AppFunctionManager
import androidx.appfunctions.ExecuteAppFunctionRequest
suspend fun callCreateNote(
context: Context,
functionId: String
) {
val manager = AppFunctionManager.getInstance(context) ?: return
val params = AppFunctionData.Builder()
.setString("title", "Weekly plan")
.setString("content", "Finish Android article")
.build()
val request = ExecuteAppFunctionRequest(
targetPackageName = "com.example.notes",
functionIdentifier = functionId,
parameters = params
)
val response = manager.executeAppFunction(request)
when (response) {
is ExecuteAppFunctionResponse.Success -> {
val result = response.result
println(result)
}
is ExecuteAppFunctionResponse.Error -> {
println(response.error)
}
}
}
这里有两个工程点要记住。
第一,函数 ID 最好不要自己手写。官方文档提到编译器会为包含 @AppFunction 的类生成一个 ID 类,比如 NoteFunctionsIds,里面会有每个函数对应的常量。调的时候优先用生成的常量,不要自己拼字符串。
第二,调用方和提供方都要把错误语义讲清楚。App Functions 不是普通内部函数,调用方很可能不是你自己写的页面,而是系统或别的 Agent,所以错误不能只丢一个 "failed"。
比如参数不合法时,应该抛更明确的异常:
import androidx.appfunctions.AppFunctionInvalidArgumentException
import androidx.appfunctions.service.AppFunction
class NoteFunctions(
private val noteRepository: NoteRepository
) {
@AppFunction
suspend fun createNote(params: CreateNoteParams): NoteDto {
if (params.title.isBlank()) {
throw AppFunctionInvalidArgumentException("title must not be blank")
}
if (params.content.isBlank()) {
throw AppFunctionInvalidArgumentException("content must not be blank")
}
val note = noteRepository.create(params.title, params.content)
return NoteDto(note.id, note.title, note.content)
}
}
官方文档这里给得很明确,应该抛 AppFunctionException 体系里的异常,而不是把所有问题都吞成未知错误。
默认在主线程跑,这个坑很大
这个点一定得单独说。官方 API reference 明确写了,@AppFunction 默认在主线程执行,AppFunctionService.executeFunction() 也是主线程入口。
所以如果你把网络、数据库、文件 IO 直接写进去,不是“可能有点慢”,而是模型就错了。
最安全的写法是把真正工作切到后台 dispatcher:
class NoteFunctions(
private val noteRepository: NoteRepository,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) {
@AppFunction
suspend fun createNote(params: CreateNoteParams): NoteDto =
withContext(ioDispatcher) {
val note = noteRepository.create(params.title, params.content)
NoteDto(note.id, note.title, note.content)
}
}
如果以后线上真用这套东西,这里会是第一批踩坑点。
运行时开关不是摆设
alpha08 的 AppFunctionManager 里已经有:
setAppFunctionEnabled()isAppFunctionEnabled()APP_FUNCTION_STATE_ENABLEDAPP_FUNCTION_STATE_DISABLEDAPP_FUNCTION_STATE_DEFAULT
这说明官方没有把 App Function 当成纯静态声明,而是把“函数当前是否可用”也纳入了运行时控制。
这在真实业务里很有用。比如:
- 某个函数依赖用户登录
- 某个函数需要会员资格
- 某个函数还在灰度
- 某个函数已经 deprecated,但暂时不能删
那就别把函数暴露和可执行混成一件事。定义是一层,运行时开关是另一层。
deprecated 不是注释问题,是协议演进问题
alpha07 开始官方支持对 AppFunction 做 deprecate。这个改动不大,但意义很重。
@AppFunction
@Deprecated("Use createRichNote(params) instead")
suspend fun createLegacyNote(params: CreateNoteParams): NoteDto {
// ...
}
因为一旦函数暴露给系统或 Agent,它就不再只是你 App 内部的私有方法,而更像一个对外协议。协议一旦被消费,就会遇到版本演进问题。
所以 App Functions 真正值得写的一点在这:它逼着 Android 开发重新认真对待能力边界,而不是把一切都藏在页面点击之后。
本地测试也不是空白区
alpha08 里已经有 AppFunctionTestRule,而且官方给了相对像样的测试路径。
先配测试编译器:
dependencies {
kspTest("androidx.appfunctions:appfunctions-compiler:1.0.0-alpha08")
testImplementation("androidx.appfunctions:appfunctions-testing:1.0.0-alpha08")
}
ksp {
arg("appfunctions:aggregateAppFunctions", "true")
}
然后可以在测试里直接发现并执行函数:
class ExampleFunctions {
@AppFunction
suspend fun add(a: Int, b: Int): Int = a + b
}
class ExampleFunctionsTest {
@get:Rule
val appFunctionTestRule = AppFunctionTestRule(context)
@Test
fun add_returnsCorrectSum() = runBlocking {
val manager = appFunctionTestRule.getAppFunctionManager()
val packageMetadata = manager.observeAppFunctions(
AppFunctionSearchSpec(
packageNames = listOf(context.packageName)
)
).first().single()
val functionMetadata = packageMetadata.appFunctions.single()
val request = ExecuteAppFunctionRequest(
targetPackageName = context.packageName,
functionIdentifier = functionMetadata.id,
parameters = AppFunctionData.Builder()
.setInt("a", 1)
.setInt("b", 2)
.build()
)
val response = manager.executeAppFunction(request)
println(response)
}
}
这套测试支持挺关键,因为 App Functions 本来就是跨边界调用。没有发现链路、执行链路、错误链路的测试,这东西很容易只剩“注解写上了,看起来挺先进”。
这东西到底值不值得现在写
值得,但写法要对。
它现在还在 alpha,所以不适合吹成“Android 下一代主流开发方式已经完全成熟”。但它已经足够值得写一篇高阶文章,因为它把几个以前分散的问题重新合在一起了:
- App 的能力到底怎么建模
- 函数描述怎么暴露给系统
- 编译期元数据怎么生成
- 系统怎么发现函数
- 调用时如何处理权限、错误和取消
- 对外暴露的能力怎么做版本演进
如果只是想追新,App Functions 可以当新闻看。但如果认真一点看,它更像 Android 正在补的一块长期基础设施。
以前 Android 的跨 App 协作主要依赖页面跳转和意图分发,调用目标更像“页面”。App Functions 往前走了一步,目标开始变成“能力”。
这一步如果真的走通,后面很多 Agent 场景才有统一入口。