flutter_app_functions 0.0.3
flutter_app_functions: ^0.0.3 copied to clipboard
Flutter plugin that mirrors the Android App Functions API. Register typed Dart functions that Android agents (Gemini and friends) can discover and invoke on-device.
flutter_app_functions #
A Flutter plugin that mirrors the Android App Functions API. Register typed Dart functions once, and any on-device agent that talks to the Android App Functions runtime (Gemini and friends) can discover, parameterise, invoke, and react to typed errors from those functions — all without leaving your existing Flutter app.
The plugin speaks the same wire protocol, the same AppFunction*Exception
types, and the same manifest shape as the official Android library, so the
documentation at developer.android.com/ai/appfunctions applies almost
verbatim.
| Android only | Minimum SDK 24, compile SDK 36 |
AndroidX appfunctions |
1.0.0-alpha08 |
| Flutter | Flutter 3.x with Dart 3.12.0+ |
| Version | 0.0.3 |
Table of contents #
- Concepts
- How it works
- Installation
- Quick start
- Declaring an app function
- Errors
- Wiring up the Android host app
- Calling a function from an agent
- Testing your app functions
- Limitations
- References
Concepts #
- App function — a single capability the agent can call on behalf of the user, e.g. create a task, search contacts, send a message.
- Definition — the schema (id, description, parameters, return type) you register on the Dart side.
- Handler — the Dart async function that runs when the agent calls the function.
- Bridge — the Kotlin side of the plugin. It exposes a single
@AppFunctionto the Android App Functions runtime and dispatches calls to the Dart registry. - Base application —
FlutterAppFunctionsApplication, theApplicationsubclass your host app extends to register the bridge with the AppFunctions system.
The plugin lives at one level of indirection on purpose: you write the
function once in Dart, and the Kotlin side dynamically forwards every
parameter and return value through the same single @AppFunction. This means
adding or removing a function does not require a Gradle rebuild.
How it works #
A single @AppFunction entry point in Kotlin accepts every call from the
agent, forwards it to the Dart registry over a MethodChannel, and returns
the result. The Kotlin side never contains user logic — it is a fixed
dispatcher. All parameters, return values, and errors flow over a JSON wire
format (KSP forbids AppFunctionData as a parameter type on
@AppFunction, so JSON strings are the only cross-language contract that
works on androidx.appfunctions:1.0.0-alpha08):
Gemini agent
↓ "call createTask(title='Buy milk')"
Android AppFunctionManager
↓ looks up @AppFunction executeAppFunction
AppFunctionsBridge.executeAppFunction() ← Kotlin (1 function, fixed)
↓ MethodChannel.invokeMethod("invokeAppFunction", {functionId, parametersJson})
FlutterAppFunctions._onMethodCall() ← Dart
↓ JSON-decode parameters, look up registry
Your handler: (context, params) async { ... } ← Dart (your logic)
↓ returns String
FlutterAppFunctions._encodeResultJson()
↓ MethodChannel Result.success("...")
AppFunctionsBridge.executeAppFunction() returns
↓
Android AppFunctionManager returns to agent
Because the handler is plain Dart, your UI state, your state-management
objects, and your ChangeNotifiers / Riverpod providers / Bloc stores are
all directly accessible — the agent can mutate the same state the user sees
on screen, and the UI rebuilds through the normal notifyListeners /
setState / ref.invalidate flow.
Installation #
Add the package to your Flutter app from pub.dev/packages/flutter_app_functions:
dependencies:
flutter_app_functions: ^0.0.3
Then run:
flutter pub get
For local development, use a path dependency:
dependencies:
flutter_app_functions:
path: ../flutter_app_functions
The plugin's Android manifest already contributes the
appfunctions:APP_FUNCTION_SERVICE permission, the
<service> declaration, and the res/xml/app_metadata.xml entry. The host
app's manifest only needs to opt in (see
Wiring up the Android host app).
Quick start #
In lib/main.dart:
import 'package:flutter/widgets.dart';
import 'package:flutter_app_functions/flutter_app_functions.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
FlutterAppFunctions.instance.register(
AppFunctionDefinition(
id: 'createTask',
description: 'Creates a new task in the user\'s task list.',
parameters: [
AppFunctionParameter.string('title'),
AppFunctionParameter.optionalString('notes'),
],
returnType: AppFunctionReturnType.string,
handler: (context, params) async {
final title = params['title'] as String;
final notes = params['notes'] as String?;
return 'Created task "$title"';
},
),
);
runApp(const MyApp());
}
In android/app/src/main/AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:appfn="http://schemas.android.com/apk/androidx.appfunctions">
<application
android:name=".MyApplication"
appfn:description="@string/appfn_description"
appfn:displayDescription="@string/appfn_display_description">
...
</application>
</manifest>
In android/app/src/main/kotlin/.../MyApplication.kt:
package com.example.myapp
import com.mohitkoley.flutter_app_functions.FlutterAppFunctionsApplication
class MyApplication : FlutterAppFunctionsApplication()
In android/app/src/main/res/values/strings.xml:
<resources>
<string name="appfn_description">com.example.myapp</string>
<string name="appfn_display_description">My App — lets the agent do useful things.</string>
</resources>
Build, install, and the function shows up in
adb shell cmd appfunctions list-app-functions.
Declaring an app function #
A function is a value of AppFunctionDefinition passed to
FlutterAppFunctions.instance.register(...):
AppFunctionDefinition(
id: 'createTask', // required
description: '...', // required, surfaced to the agent
parameters: [ ... ], // optional, defaults to []
returnType: AppFunctionReturnType.string, // optional, defaults to void
handler: (context, params) async { ... }, // required
)
idmust be unique within the registry. Re-registering with the same id replaces the existing definition.descriptionis KDoc-style documentation of the function. It is exposed to the agent verbatim, so write it the way you would write a public API docstring.parametersis the ordered list of accepted parameters (see below).returnTypedefaults toAppFunctionReturnType.voidType. Set it explicitly for any function that returns data.handleris the async Dart function executed when the agent calls the function. It receives:context— anAppFunctionContextwith thefunctionIdand the validated, type-coerced parameter map.parameters— the same validated map, for convenience. It should return the declared return value, ornullfor void returns.
Parameters #
Each parameter is an AppFunctionParameter. The supported scalar types map
1:1 to androidx.appfunctions.AppFunctionData:
| Dart factory | Wire type | Description |
|---|---|---|
AppFunctionParameter.string(name, ...) |
String |
UTF-8 string. |
AppFunctionParameter.optionalString(name, ...) |
String? |
Optional string. |
AppFunctionParameter.int(name, ...) |
int64 |
64-bit signed integer. |
AppFunctionParameter.double(name, ...) |
double |
IEEE-754 double. |
AppFunctionParameter.bool(name, ...) |
bool |
Boolean. |
AppFunctionParameter.stringList(name, ...) |
List<String> |
Ordered list of strings. |
Optional parameters default to required: true; pass required: false to
mark a string as optional:
AppFunctionParameter.string('filter', required: false)
String parameters can be restricted to an enum-like set:
AppFunctionParameter.string(
'filterType',
description: 'Either "INDIVIDUAL" or "GROUP".',
enumValues: ['INDIVIDUAL', 'GROUP'],
)
Enum violations, missing required values, wrong types, and unknown keys all
raise AppFunctionInvalidArgumentException and are surfaced to the agent as
a typed androidx.appfunctions.AppFunctionInvalidArgumentException.
Return types #
AppFunctionReturnType mirrors the same scalar set plus a void marker:
AppFunctionReturnType.voidType // handler returns null/void
AppFunctionReturnType.string // handler returns String
AppFunctionReturnType.int64 // handler returns int
AppFunctionReturnType.double // handler returns double
AppFunctionReturnType.boolean // handler returns bool
AppFunctionReturnType.stringList // handler returns List<String>
Handlers that return a different type throw
AppFunctionAppUnknownException and the agent sees the same typed error
it would from a native App Function.
Errors #
The Dart exception hierarchy maps 1:1 to androidx.appfunctions:
| Dart exception | Kotlin exception | Error code |
|---|---|---|
AppFunctionInvalidArgumentException |
AppFunctionInvalidArgumentException |
AppFunctionInvalidArgument |
AppFunctionElementNotFoundException |
AppFunctionElementNotFoundException |
AppFunctionElementNotFound |
AppFunctionFunctionNotFoundException |
AppFunctionFunctionNotFoundException |
AppFunctionFunctionNotFound |
AppFunctionNotSupportedException |
AppFunctionNotSupportedException |
AppFunctionNotSupported |
AppFunctionPermissionRequiredException |
AppFunctionPermissionRequiredException |
AppFunctionPermissionRequired |
AppFunctionDisabledException |
AppFunctionDisabledException |
AppFunctionDisabled |
AppFunctionAppUnknownException |
AppFunctionAppUnknownException |
AppFunctionAppUnknown |
AppFunctionPlatformNotSupportedException (extends UnsupportedError) |
n/a — fired on iOS/macOS/Linux/Windows/Web before native code is touched | n/a |
Throw any of these from your handler and the agent sees the matching typed exception on the Kotlin side:
handler: (context, params) async {
if (!userHasAccess) {
throw AppFunctionPermissionRequiredException('User has not granted access.');
}
...
}
Handlers that throw any other error are wrapped as
AppFunctionAppUnknownException.
Wiring up the Android host app #
The plugin takes care of every manifest entry inside the
<application> element, so the host app's manifest only needs to:
- Declare the
xmlns:appfnnamespace. - Set
android:name=".MyApplication"(or your equivalent) on<application>. - Provide
appfn:descriptionandappfn:displayDescriptionattributes on<application>. - Override the
appfn_description/appfn_display_descriptionstrings in yourres/values/strings.xml(the plugin ships sensible defaults so step 4 is optional).
Then point your custom Application class at
FlutterAppFunctionsApplication:
class MyApplication : FlutterAppFunctionsApplication()
That's the only Kotlin code you need to write. FlutterAppFunctionsApplication
implements AppFunctionConfiguration.Provider and registers the
AppFunctionsBridge with the AppFunctions runtime.
Gradle #
If your app's own modules declare their own @AppFunctions, add the
following to your app-level android/app/build.gradle.kts so the KSP
processor aggregates them all into a single metadata file:
ksp {
arg("appfunctions:aggregateAppFunctions", "true")
}
(For pure plugin users, this block is already added to the plugin's
android/build.gradle.kts.)
Calling a function from an agent #
The agent interacts with the Kotlin bridge; the plugin takes care of the Dart round-trip. Once your app is installed:
# List every function the bridge exposes:
adb shell cmd appfunctions list-app-functions
# Invoke a function by id:
adb shell cmd appfunctions execute-app-function \
--uri appfunctions://com.mohitkoley.flutter_app_functions/executeAppFunction \
--function-id createTask \
--params '{"title":"Buy milk","notes":"2L semi-skimmed"}'
The agent (e.g. Gemini) sees the function descriptions and parameters as
ordinary Android AppFunctions and calls them through the standard
AppFunctionManager flow.
Testing your app functions #
Dart #
flutter test
The suite covers the registry, the type-coercion validator, the
exception-hierarchy mapping, and the round-trip through the method
channel. The method channel is mocked via
TestDefaultBinaryMessengerBinding.setMockMethodCallHandler.
Kotlin #
cd example/android
./gradlew testDebugUnitTest
The Kotlin unit tests cover the plugin's lifecycle (getPlatformVersion).
The suspend executeAppFunction entry point is covered indirectly by
the integration test because it requires a real Flutter engine channel,
and the alpha08 AppFunction*Exception subclasses cannot be
constructed in plain JVM tests (their constructors touch
android.os.Bundle.EMPTY, which is only initialised inside a real
Android runtime).
Integration #
cd example
flutter test integration_test
Drives the example app's plugin, registering sample functions and exercising the method channel from the host side.
Limitations #
- Android only.
androidx.appfunctionshas no iOS, macOS, Linux, Windows, or Web counterpart.FlutterAppFunctions.register,registerAll,invoke, andgetPlatformVersionthrowAppFunctionPlatformNotSupportedExceptionon any non-AndroiddefaultTargetPlatformbefore the registry is mutated or any method channel traffic is generated. The exception'splatformfield reports the offending runtime (e.g."iOS"). If you want to share your function definitions between an Android build and an iOS / Web build of the same codebase, gate theregistercall ondefaultTargetPlatform == TargetPlatform.android. - The plugin targets
androidx.appfunctions:1.0.0-alpha08, which is an alpha release of the AppFunctions library. - Nested object and array-of-object parameters are not supported — only
the scalar types listed above. The
AppFunctionDatawire format supports richer shapes; the plugin exposes the common subset to keep the Dart surface ergonomic. - Boolean and double are exposed as
boolean/doublein the return type, matching the official App Functions API.