{"slug": "appfunctions-revisited", "title": "AppFunctions, revisited", "summary": "An Android developer updated the Be Nice app to use Jetpack AppFunctions alpha10, which removed the appfunctions-service artifact and requires hosting an @AppFunctionServiceEntryPoint AppFunctionService. The implementation now matches app display names instead of package names, and the developer corrected an earlier article's claim about package name usage.", "body_md": "In [Agentic interaction using AppFunctions](https://dev.to/tkuenneth/agentic-interaction-using-appfunctions-m8k) I showed how *Be nice* publishes `createAppPair`\n\nfor agents, using Jetpack appfunctions **1.0.0-alpha08**. That setup leaned on a library-merged `PlatformAppFunctionService`\n\n, an `AppFunctionContext`\n\nparameter, and aggregate XML named `app_functions.xml`\n\n/ `app_functions_v2.xml`\n\n. When I bumped toward what Android Studio was suggesting as of mid July, Sync failed:\n\n```\nCould not find androidx.appfunctions:appfunctions-service:1.0.0-alpha10.\n```\n\nOn [1 July 2026](https://developer.android.com/jetpack/androidx/releases/appfunctions), **alpha10** stopped publishing `appfunctions-service`\n\n(Maven still lists it through **alpha09**). The [Add the AppFunctions API](https://developer.android.com/ai/appfunctions/add-appfunctions) guide now expects you to host an `@AppFunctionServiceEntryPoint`\n\n`AppFunctionService`\n\n. Spoiler: that is the real change. The rest of this post is how that landed in *Be nice*, plus an important update to the earlier article that is easy to miss if you only chase compile errors.\n\nIn the earlier post I wrote that agents would pass **package names** for `app1`\n\nand `app2`\n\n. That was wrong for *Be nice*. The implementation matches **display names**, case-insensitively, against installed apps. Package names are a convenient engineer habit; they are a poor agent habit when the user said “Clock” and “Contacts”. KDoc and app-level metadata have to say what the code accepts — agents lean on that text harder than on your mental model of the APK.\n\nBefore alpha10 you annotated a helper class and let the service AAR merge a stock entry point into the manifest. Starting with alpha10:\n\n`@AppFunction`\n\nlives in `androidx.appfunctions`\n\n(not `androidx.appfunctions.service`\n\n)`AppFunctionService`\n\nmarked `@AppFunctionServiceEntryPoint`\n\n`serviceName`\n\nand the XML named in `appFunctionXmlFileName`\n\n`AppFunctionContext`\n\nparameter; the service `Context`\n\n`appfunctions:aggregateAppFunctions`\n\n. Grepping for empty `app_functions.xml`\n\n/ `app_functions_v2.xml`\n\nis no longer the success signalAlpha09 still shipped the service artifact. Jumping from the first article to today skips that soft landing and hits alpha10.\n\n```\n[versions]\nappfunctions = \"1.0.0-alpha10\"\n\n[libraries]\nandroidx-appfunctions = { group = \"androidx.appfunctions\", name = \"appfunctions\", version.ref = \"appfunctions\" }\nandroidx-appfunctions-compiler = { group = \"androidx.appfunctions\", name = \"appfunctions-compiler\", version.ref = \"appfunctions\" }\ndependencies {\n    implementation(libs.androidx.appfunctions)\n    ksp(libs.androidx.appfunctions.compiler)\n}\n```\n\n`compileSdk`\n\nstays at 36 or higher (*Be nice* is on 37). I still keep each `merge*Assets`\n\ntask depending on the matching `ksp*Kotlin`\n\ntask. Alpha10 still emits XML under KSP-generated assets — `benice_app_functions.xml`\n\nfor me — and on my AGP/KSP combo packaging was flaky without that edge. Same class of problem as in the first article; new file name.\n\n```\n@RequiresApi(Build.VERSION_CODES.BAKLAVA)\n@AppFunctionServiceEntryPoint(\n    serviceName = \"BeNiceAppFunctionService\",\n    appFunctionXmlFileName = \"benice_app_functions\",\n)\nabstract class BeNiceFunctions : AppFunctionService() {\n\n    /**\n     * Launches two installed apps together in split screen (an app pair).\n     *\n     * @param app1 Name of the first app in the pair.\n     * @param app2 Name of the second app in the pair.\n     * @return A localized message describing success or failure.\n     */\n    @AppFunction(isDescribedByKDoc = true)\n    suspend fun createAppPair(\n        app1: String,\n        app2: String\n    ): String = withContext(Dispatchers.IO) {\n        val success = performPairing(applicationContext, app1, app2)\n        if (success) {\n            getString(R.string.pair_created_success, app1, app2)\n        } else {\n            getString(R.string.pair_created_failure)\n        }\n    }\n}\n```\n\n`AppFunctionService`\n\nis API 36 (Baklava), so the abstract host carries `@RequiresApi`\n\n. Where the old code used `context.context`\n\n, I use `applicationContext`\n\n/ `getString`\n\non the service.\n\nI still use `suspend`\n\n. The docs also say AppFunctions run on the **UI thread** unless you move work elsewhere. Resolving installed apps and publishing a dynamic shortcut is not UI-thread work; `withContext(Dispatchers.IO)`\n\nis not optional once the body does real I/O.\n\n`isDescribedByKDoc = true`\n\nstill folds KDoc into agent metadata. Treat that block as public API surface for a model, not as a comment for the next human on your team.\n\nOfficial snippets often put `@AndroidEntryPoint`\n\nand `@Inject`\n\non the abstract service. I did not. KSP already generates a subclass (`BeNiceAppFunctionService`\n\n). Hilt would want to subclass the same type for injection. Stacking two code generators on one service is a fight I do not need for two collaborators. A small `@EntryPoint`\n\nplus `EntryPointAccessors.fromApplication`\n\nfor `InstalledAppsManager`\n\nand `ShortcutsRepository`\n\nkeeps the host boring.\n\nPoint at the generated class and at the XML name you chose:\n\n```\n<service\n    android:name=\".appfunctions.BeNiceAppFunctionService\"\n    android:exported=\"true\"\n    android:permission=\"android.permission.BIND_APP_FUNCTION_SERVICE\"\n    tools:targetApi=\"36\">\n    <property\n        android:name=\"android.app.appfunctions.schema\"\n        android:value=\"app_functions_schema.xsd\" />\n    <property\n        android:name=\"android.app.appfunctions.v2\"\n        android:value=\"benice_app_functions.xml\" />\n    <intent-filter>\n        <action android:name=\"android.app.appfunctions.AppFunctionService\" />\n    </intent-filter>\n</service>\n```\n\nThe `v2`\n\nproperty value is `appFunctionXmlFileName`\n\nplus `.xml`\n\n. Wrong name, silent packaging miss; especially if you still grep for `app_functions_v2.xml`\n\n.\n\nUnder `<application>`\n\nthe guide also wants app-level metadata. That is where I put the display-name contract in writing:\n\n```\n<property\n    android:name=\"android.app.appfunctions.app_metadata\"\n    android:resource=\"@xml/app_metadata\" />\n<AppFunctionAppMetadata xmlns:appfn=\"http://schemas.android.com/apk/androidx.appfunctions\"\n    appfn:description=\"This app creates app pairs that launch two installed apps together in split screen.\n    Operational Patterns:\n    - Use 'createAppPair' with the display names of two installed apps.\n    Constraints:\n    - Both app names must match installed apps (case-insensitive).\"\n    appfn:displayDescription=\"@string/appfunctions_display_description\" />\n```\n\n`description`\n\nis for agents; `displayDescription`\n\nis for people. The resolver can return `null`\n\nif the property is absent. I would not ship without it: this is how you stop an agent from inventing package-name parameters your code never accepted.\n\nAfter a debug build, the generated assets directory should show `benice_app_functions.xml`\n\n, not the old aggregate pair. If empty `app_functions.xml`\n\n/ `app_functions_v2.xml`\n\nare still what you find, the aggregate KSP flag is probably still on.\n\n```\nunzip -l app/build/outputs/apk/debug/app-debug.apk | grep -E 'benice_app_functions|app_functions'\n```\n\nYou want `benice_app_functions.xml`\n\nin the APK. Once that is true, install and check the function on device.\n\nAfter install, the same `adb`\n\npath from the first article still works. On a debug build the package id carries the `.debug`\n\nsuffix; release stays `de.thomaskuenneth.benice`\n\n. Grep with the shared prefix either way:\n\n```\nadb shell cmd app_function list-app-functions | grep -F de.thomaskuenneth.benice\n```\n\nYou want the stable id `de.thomaskuenneth.benice.appfunctions.BeNiceFunctions#createAppPair`\n\n. Do not take the first `something#something`\n\nmatch blindly — the current list dump also contains other `#`\n\nfragments, and a wrong id comes back as `App function not found`\n\n.\n\nExecute with AppSearch-style `--parameters`\n\n(each string as a one-element JSON array). Use **display names**, not package names:\n\n```\nadb shell \"cmd app_function execute-app-function \\\n  --package de.thomaskuenneth.benice.debug \\\n  --function 'de.thomaskuenneth.benice.appfunctions.BeNiceFunctions#createAppPair' \\\n  --parameters '{\\\"app1\\\":[\\\"Settings\\\"],\\\"app2\\\":[\\\"Clock\\\"]}'\"\n```\n\nThat returned the localized success string on my API 37 emulator. The same call with `com.foo`\n\n/ `com.bar`\n\nstill runs the function, but resolves to the failure string — which is what you want if the names are wrong. Swap `--package`\n\nto `de.thomaskuenneth.benice`\n\nfor a release install. Some API 36 images still lack the shell until you pick a revision that includes it (I needed 36.1 last time).\n\n`appfunctions-service`\n\nand `aggregateAppFunctions`\n\n`@AppFunction`\n\nonto an `@AppFunctionServiceEntryPoint`\n\n`AppFunctionService`\n\n; drop `AppFunctionContext`\n\n`app_metadata`\n\nin the manifest`adb`\n\non a capable image`suspend`\n\n; put blocking work on a background dispatcherIf you are already on alpha09, Android Studio documents a migration skill for the hop. From alpha08 you touch the same surfaces, and you also leave behind anything that assumed the service AAR and aggregate assets.\n\nThe first article is still useful for motivation, R8 notes, and the habit of unzipping the APK. Its dependency list, `AppFunctionContext`\n\nsignature, aggregate greps, and the package-name claim for *Be nice* are not. Day to day I treat the [Add the AppFunctions API](https://developer.android.com/ai/appfunctions/add-appfunctions) guide and the [alpha10 release notes](https://developer.android.com/jetpack/androidx/releases/appfunctions) as source of truth — and I re-read them on every bump. The gap between alpha08 and alpha10 is the kind of gap alphas are for: the library stopped hosting your functions, so your app has to.\n\n`1.0.0-alpha10`\n\n)`AppFunctionApiSnippets.kt`", "url": "https://wpnews.pro/news/appfunctions-revisited", "canonical_source": "https://dev.to/tkuenneth/appfunctions-revisited-4n90", "published_at": "2026-07-25 11:36:27+00:00", "updated_at": "2026-07-25 12:01:28.987056+00:00", "lang": "en", "topics": ["developer-tools", "artificial-intelligence", "ai-agents"], "entities": ["Be Nice", "Jetpack AppFunctions", "Android Studio", "Google", "Maven"], "alternates": {"html": "https://wpnews.pro/news/appfunctions-revisited", "markdown": "https://wpnews.pro/news/appfunctions-revisited.md", "text": "https://wpnews.pro/news/appfunctions-revisited.txt", "jsonld": "https://wpnews.pro/news/appfunctions-revisited.jsonld"}}