Android App Functions 深入理解

0 阅读8分钟

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

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

第二种通常更适合往长期演进。因为一旦后面要补字段,比如 tagsfolderIdpinnedsource,参数对象能稳住接口形态,也更适合和 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)
        }
}

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

运行时开关不是摆设

alpha08AppFunctionManager 里已经有:

  • setAppFunctionEnabled()
  • isAppFunctionEnabled()
  • APP_FUNCTION_STATE_ENABLED
  • APP_FUNCTION_STATE_DISABLED
  • APP_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 场景才有统一入口。