Android App Functions 深入理解

App Functions 可以把 App 里原本藏在页面背后的能力,变成一组可以被系统或 Agent 发现、理解、执行的函数。

这件事的重点不在"AI"两个字,而在接口形态变了。过去 Android App 的主入口是 ActivityDeep LinkIntent。现在官方开始给另一套入口:函数调用。

如果一个记事 App 暴露了 createNote(),一个提醒 App 暴露了 scheduleReminder(),那系统或者 Agent 理论上就可以不经过层层 UI 点击,直接完成跨 App 编排。

这也是它比普通 Android 题更高阶的地方。它讨论的不是界面,不是状态管理,不是某个组件怎么用,而是 App 的能力建模、函数元数据、编译期生成、服务暴露、跨进程执行、可观测发现、运行时开关和测试。

先看官方现在的依赖。按 2026-04-11 这个时间点,AndroidX 最新是 1.0.0-alpha08,发布时间是 2026-03-11

kotlin 复制代码
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

kotlin 复制代码
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 来说,这些描述不是装饰,而是理解能力边界的一部分。

返回类型通常应该保持干净、可序列化、不要夹带太多平台对象。比如可以这么定义:

kotlin 复制代码
import androidx.appfunctions.AppFunctionSerializable

@AppFunctionSerializable
data class NoteDto(
    val id: Long,
    val title: String,
    val content: String
)

参数对象也一样,尽量做成明确的数据结构,不要一股脑塞 Map<String, Any>

kotlin 复制代码
@AppFunctionSerializable
data class CreateNoteParams(
    val title: String,
    val content: String
)

然后把函数签名收敛一下:

kotlin 复制代码
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 里加服务:

xml 复制代码
<service
    android:name=".NoteAppFunctionService"
    android:permission="android.permission.BIND_APP_FUNCTION_SERVICE">
    <intent-filter>
        <action android:name="android.app.appfunctions.AppFunctionService" />
    </intent-filter>
</service>

服务本身长这样:

kotlin 复制代码
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 结构,只是多暴露一个面向系统的函数入口。

建模比调用更重要

很多人看到这种能力,第一反应是"怎么执行函数"。但真正难的通常不是执行,而是建模。

比如下面这两个函数,技术上都能工作:

kotlin 复制代码
@AppFunction
suspend fun createNote(title: String, content: String): NoteDto

@AppFunction
suspend fun createNote(params: CreateNoteParams): NoteDto

第二种通常更适合往长期演进。因为一旦后面要补字段,比如 tagsfolderIdpinnedsource,参数对象能稳住接口形态,也更适合和 schema 对齐。

如果函数准备长期暴露给 Agent,用 schema 来描述会更清楚。alpha08 里新加了 @AppFunctionSchemaDefinition,官方给的是这种方向:

kotlin 复制代码
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 已经支持观察可用函数元数据:

kotlin 复制代码
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 形态的示意代码,重点看调用模型:

kotlin 复制代码
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"

比如参数不合法时,应该抛更明确的异常:

kotlin 复制代码
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:

kotlin 复制代码
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)
        }
}

如果以后线上真用这套东西,这里会是第一批踩坑点。

运行时开关不是摆设

alpha08AppFunctionManager 里已经有:

  • setAppFunctionEnabled()
  • isAppFunctionEnabled()
  • APP_FUNCTION_STATE_ENABLED
  • APP_FUNCTION_STATE_DISABLED
  • APP_FUNCTION_STATE_DEFAULT

这说明官方没有把 App Function 当成纯静态声明,而是把"函数当前是否可用"也纳入了运行时控制。

这在真实业务里很有用。比如:

  • 某个函数依赖用户登录
  • 某个函数需要会员资格
  • 某个函数还在灰度
  • 某个函数已经 deprecated,但暂时不能删

那就别把函数暴露和可执行混成一件事。定义是一层,运行时开关是另一层。

deprecated 不是注释问题,是协议演进问题

alpha07 开始官方支持对 AppFunction 做 deprecate。这个改动不大,但意义很重。

kotlin 复制代码
@AppFunction
@Deprecated("Use createRichNote(params) instead")
suspend fun createLegacyNote(params: CreateNoteParams): NoteDto {
    // ...
}

因为一旦函数暴露给系统或 Agent,它就不再只是你 App 内部的私有方法,而更像一个对外协议。协议一旦被消费,就会遇到版本演进问题。

所以 App Functions 真正值得写的一点在这:它逼着 Android 开发重新认真对待能力边界,而不是把一切都藏在页面点击之后。

本地测试也不是空白区

alpha08 里已经有 AppFunctionTestRule,而且官方给了相对像样的测试路径。

先配测试编译器:

kotlin 复制代码
dependencies {
    kspTest("androidx.appfunctions:appfunctions-compiler:1.0.0-alpha08")
    testImplementation("androidx.appfunctions:appfunctions-testing:1.0.0-alpha08")
}

ksp {
    arg("appfunctions:aggregateAppFunctions", "true")
}

然后可以在测试里直接发现并执行函数:

kotlin 复制代码
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 场景才有统一入口。

相关推荐
小妖66611 分钟前
怎么用 tauri 创建编译 android 应用程序
android·tauri
鸟儿不吃草1 小时前
安卓实现左右布局聊天界面
android·开发语言·python
xxjj998a3 小时前
Laravel 1.x:PHP框架的原始魅力
android·php·laravel
formula100003 小时前
在iOS/安卓上远程连接任何 Agent!Claude、Codex、Copilot、Gemini、OpenCode 等
android·copilot
该用户可能存在3 小时前
Blbl-android 更新至 v0.1.24,体验更流畅、更稳定
android·哔哩哔哩·电视app·androidtv·bbll·blbl·bilibilitv
lKWO OMET3 小时前
mysql之字符串函数
android·数据库·mysql
liang_jy14 小时前
Android SparseArray
android·源码
liang_jy14 小时前
Activity 启动流程扩展篇(一)—— startActivityInner 任务决策全解析
android·源码
NPE~15 小时前
[App逆向]脱壳实战
android·教程·逆向·android逆向·逆向分析
木易 士心16 小时前
别再只会用 drawCircle 了!一文搞懂 Android Canvas 底层机制
android