Skip to main content

flutter_app_copilot 0.3.0 copy "flutter_app_copilot: ^0.3.0" to clipboard
flutter_app_copilot: ^0.3.0 copied to clipboard

Let users control your Flutter app through chat. Turn requests like "turn on dark mode" into actions your app runs, using Gemini, OpenAI, or Claude.

Flutter App Copilot #

This package lets your app's users prompt AI so it can execute actions and operations inside your app, such as:

  • "Turn on dark mode" → change an app setting.
  • "Change my name to Maya" → update a profile.
  • "Take me to my orders" → navigate to a screen.

You define the actions your app exposes. The package connects to Gemini, OpenAI, or Claude, runs model-requested actions through your handler, and returns the model's final reply. Your app keeps control of its business rules, data, and UI.

Demo #

Demo of the AI agent turning on dark mode and changing the user's name from chat messages

In this demo, a user chats with the in-app agent and the app responds:

  1. "Turn dark mode on" switches the app's theme.
  2. "Change my name to Joe" updates the profile and confirms the change.

Each request runs through your own AiActionHandler, so the model only does what your app exposes.

Features #

Feature What it provides
One provider interface Use AIService with Gemini, OpenAI, or Claude.
Function calling Connect model requests to your existing app logic.
Typed failures Distinguish invalid input, request errors, and unusable replies.
Optional navigation context Describe the current GoRouter stack and available routes.
Ready-made navigation action Let the model reset the stack, push, replace, and pop on your GoRouter.

How it works #

  1. Your app sends a message through AIService.send().
  2. The model requests a function, such as add_numbers, with JSON arguments.
  3. Your AiActionHandler validates the arguments and calls the matching action.
  4. The package sends the action's result back to the model.
  5. This repeats until the model returns its final text reply, with a limit of 12 tool rounds per request.

Installation #

Requires Flutter with Dart >=3.13.7 <4.0.0. Dependencies may impose additional platform requirements.

flutter pub add flutter_app_copilot

For a local copy, use a path: dependency instead. Import the public API:

import 'package:flutter_app_copilot/flutter_app_copilot.dart';

Quick start #

Your integration has three pieces:

Piece Your responsibility
FunctionDefinition Describe each tool's name, purpose, and expected arguments.
AiExecutableAction Implement the operation using your app's logic.
AiActionHandler Match the function name, parse its arguments, and execute the action.

The complete calculator example defines functionDefinitionList, AddNumbersAction, and CalculatorActionHandler. After defining those pieces in your app, connect and call them like this:

final ai = AIServiceFactory.create(
  provider: AIProvider.openAI,
  apiKey: apiKey,
  modelName: modelName,
  configuration: AIConfiguration(
    systemPrompt: 'Use add_numbers for addition. Reply with the result.',
    functionDefinitions: functionDefinitionList,
  ),
  actionHandler: CalculatorActionHandler([AddNumbersAction()]),
);

final messages = [ChatMessage.user('What is 12 plus 30?')];
final reply = await ai.send(messages);
messages.add(ChatMessage.model(reply));

Supply your provider's API key and a model ID available to your account. See the example guide for the flow and customization notes.

Implementing actions #

Actions receive a typed input and return Either<Failure, Output>:

class AddNumbersAction implements AiExecutableAction<AddNumbersInput, num, Error> {
  @override
  Future<Either<Error, num>> execute(AddNumbersInput input) async =>
      Right(input.numbers.fold<num>(0, (sum, number) => sum + number));
}

AddNumbersInput is defined by the example, not the package. Its fromJson method parses the actual values in response.functionArgs into a typed input. FunctionDefinition describes the schema the model uses to produce those values.

In your handler, match the function name and select the registered action, then:

final input = AddNumbersInput.fromJson(response.functionArgs);
if (input.isLeft) {
  return Left(input.left);
}

final result = await action.execute(input.right);
return result.map((sum) => sum.toString());

Return Right(result) on success or Left(error) for expected action failures. The package sends either result back to the model so it can continue or explain what went wrong. Adding an action to actions alone does not register its function definition or dispatch its calls; implement both explicitly.

Providers #

Provider Factory value Setup
OpenAI AIProvider.openAI Supply a non-empty API key.
Claude AIProvider.claude Supply a non-empty API key.
Gemini AIProvider.gemini Supply a non-empty Gemini API key.

modelName is optional. Omitting it or passing a blank value uses the provider's packaged default, available through AIProvider.defaultModelName. To switch providers, change the factory value and supply the matching credentials and model ID.

Add go_router as a direct dependency and import it alongside this package:

import 'package:go_router/go_router.dart';

final ai = AIServiceFactory.create(
  provider: AIProvider.openAI,
  apiKey: apiKey,
  configuration: AIConfiguration(
    functionDefinitions: functionDefinitionList,
    contextSources: [GoRouterContextSource(router)],
  ),
  actionHandler: actionHandler,
);

final reply = await ai.send(messages);

Pass the router once. GoRouterContextSource reads it on every send(), so the model always sees the current stack. Keep using the same GoRouter the app navigates with; a router created later is not picked up.

The exported GoRouterStateContext extension provides currentStack(), routeTree(), routePaths(), routeParameterShapes(), and getRouterContext(). This supplies route information as text. Dynamic route placeholders are also listed with their argument shape, such as /products/:productId with {"productId": "<string>"} (optional :id? placeholders are marked (optional)). Path parameter values are always strings.

Other context #

Implement AppContextSource to give the model any other live app state:

class SelectedProductContext implements AppContextSource {
  @override
  String read() => '\nSelected product: ${selectedProduct.name}\n';
}

Sources are read in list order on every send(), and their text is appended only to the outgoing copy of the final user message. send(additionalContext: ...) still works for one-off text and is added after the sources. If a source throws, send() throws AIProviderContextFailure and sends no request.

Register GoRouterNavigationAction and its four tools: resetStackTo, pushScreen, replaceTopScreen, and popScreen.

final navigation = GoRouterNavigationAction(router);

final configuration = AIConfiguration(
  functionDefinitions: [
    ...appFunctionDefinitions,
    ...navigationFunctionDefinitions(locations: router.routePaths()),
  ],
);

// Inside your AiActionHandler.handle:
final method = NavigationMethod.fromFunctionName(response.functionName);
if (method != null) {
  final input = NavigationInput.fromArgs(method, response.functionArgs);
  if (input.isLeft) return Left(input.left);
  return navigation.execute(input.right);
}

locations limits which paths the model can open; leave it out to allow any. The model fills :placeholders through pathParameters.

Every call is checked against your routes. If the route is unknown, a placeholder has no value, or there is nothing to pop, the action returns a NavigationFailure the model can read instead of navigating. Routes inside a StatefulShellRoute always open with resetStackTo, because pushing them over a page outside the shell crashes the Navigator. Navigations are spaced by pauseBetweenNavigations (2 seconds by default; use Duration.zero in tests). extra and pop results are not supported, since model arguments are plain JSON.

Handling failures #

Catch AIProviderFailure at your application's logic boundary and convert it into a failure result or presentation state:

Future<Either<AIProviderFailure, String>> sendSafely(
  AIService ai,
  List<ChatMessage> messages,
) async {
  try {
    return Right(await ai.send(messages));
  } on AIProviderFailure catch (failure) {
    return Left(failure);
  }
}
Failure Meaning
AIProviderInvalidInputFailure Empty conversation, blank user text, or an assistant message at the end.
AIProviderRequestFailure A network or provider request failed.
AIProviderTooManyCallsFailure The model reached the tool-round limit.
AIProviderMalformedToolCallFailure A tool call had missing or unreadable fields.
AIProviderEmptyResponseFailure The model returned no usable text.
AIProviderInvalidResponseFailure The provider response had an unexpected shape.

Failures expose code, a stable reason, and an optional diagnostic message. Branch on the failure type or stable fields rather than its human-readable text. Validate setup separately: a missing or blank apiKey throws StateError during creation. Errors thrown by your own action handler also need handling at your app boundary.

Responsibilities and scope #

The package owns Your app owns
Provider requests and response parsing Provider selection, credentials, and Firebase setup
Tool-call continuation and provider failures Function definitions, argument validation, and actions
Chat models and route-context formatting Conversation state, storage, and UI
Optional GoRouter navigation action and its tools Your routes and which ones the model may open

The API returns complete text replies. Streaming, request cancellation, conversation persistence, offline replies, and cache fallback are not included. Import supported types from flutter_app_copilot.dart; lib/src/ contains implementation details.

License #

MIT. Copyright © 2026 Yousef Shams.

0
likes
160
points
--
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Let users control your Flutter app through chat. Turn requests like "turn on dark mode" into actions your app runs, using Gemini, OpenAI, or Claude.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

dio, either_dart, flutter, go_router

More

Packages that depend on flutter_app_copilot