flutter_app_functions 0.0.7 copy "flutter_app_functions: ^0.0.7" to clipboard
flutter_app_functions: ^0.0.7 copied to clipboard

PlatformAndroid

Flutter bridge for Android App Functions. Register typed Dart functions that Android agents such as Gemini can discover and invoke on-device.

example/lib/main.dart

import 'dart:convert';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_app_functions/flutter_app_functions.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();

  final demoStore = DemoProductivityStore();
  registerProductivityAppFunctions(demoStore);

  runApp(MyApp(store: demoStore));
}

void registerProductivityAppFunctions(DemoProductivityStore store) {
  final appFunctions = FlutterAppFunctions.instance;

  if (defaultTargetPlatform != TargetPlatform.android) {
    appFunctions.ensureInitialized();
    return;
  }

  appFunctions
    ..register(
      AppFunctionDefinition(
        id: 'createTask',
        description:
            'Create a task or reminder in the user\'s productivity app. '
            'Use this when the user asks to remember, schedule, or track work.',
        parameters: [
          AppFunctionParameter.string(
            'title',
            description: 'Short task title, for example "Pick up package".',
          ),
          AppFunctionParameter.optionalString(
            'dueDateTime',
            description:
                'Natural language or ISO-like due time, for example "today 5 PM".',
          ),
          AppFunctionParameter.optionalString(
            'location',
            description: 'Where the task should happen, for example "Work".',
          ),
        ],
        returnType: AppFunctionReturnType.string,
        handler: (context, params) async {
          final task = store.createTask(
            title: params['title'] as String,
            dueDateTime: params['dueDateTime'] as String?,
            location: params['location'] as String?,
          );
          return jsonEncode(task.toJson());
        },
      ),
    )
    ..register(
      AppFunctionDefinition(
        id: 'addItemsToShoppingList',
        description:
            'Add grocery or shopping items to the active shopping list. '
            'Use this after extracting ingredients or product names from a user request.',
        parameters: [
          AppFunctionParameter.stringList(
            'items',
            description:
                'Names of items to add, for example ["noodles", "soy sauce"].',
          ),
          AppFunctionParameter.optionalString(
            'source',
            description:
                'Where the items came from, for example "Lisa noodle recipe".',
          ),
        ],
        returnType: AppFunctionReturnType.stringList,
        handler: (context, params) async {
          final addedItems = store.addShoppingItems(
            items: params['items'] as List<String>,
            source: params['source'] as String?,
          );
          return addedItems.map((item) => item.label).toList();
        },
      ),
    )
    ..register(
      AppFunctionDefinition(
        id: 'completeTask',
        description:
            'Mark one task as complete. Use this when the user says a task is done.',
        parameters: [
          AppFunctionParameter.int(
            'taskId',
            description: 'The numeric id of the task to complete.',
          ),
        ],
        returnType: AppFunctionReturnType.string,
        handler: (context, params) async {
          final taskId = params['taskId'] as int;
          final completed = store.completeTask(taskId);
          if (completed == null) {
            throw AppFunctionElementNotFoundException(
              'No task exists with id $taskId.',
            );
          }
          return jsonEncode(completed.toJson());
        },
      ),
    )
    ..register(
      AppFunctionDefinition(
        id: 'summarizeToday',
        description:
            'Summarize the user\'s current tasks and shopping list for today.',
        returnType: AppFunctionReturnType.string,
        handler: (context, params) async => store.summaryForAgent(),
      ),
    );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key, required this.store});

  final DemoProductivityStore store;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter AppFunctions MCP Example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
        useMaterial3: true,
      ),
      home: ProductivityAgentDemo(store: store),
    );
  }
}

class ProductivityAgentDemo extends StatefulWidget {
  const ProductivityAgentDemo({super.key, required this.store});

  final DemoProductivityStore store;

  @override
  State<ProductivityAgentDemo> createState() => _ProductivityAgentDemoState();
}

class _ProductivityAgentDemoState extends State<ProductivityAgentDemo> {
  final TextEditingController _promptController = TextEditingController(
    text: 'Remind me to pick up my package at work today at 5 PM',
  );
  final List<AgentLogEntry> _logs = <AgentLogEntry>[];
  bool _running = false;

  @override
  void dispose() {
    _promptController.dispose();
    super.dispose();
  }

  Future<void> _runSimulatedAgent() async {
    final prompt = _promptController.text.trim();
    if (prompt.isEmpty || _running) return;

    setState(() {
      _running = true;
      _logs.insert(0, AgentLogEntry.user(prompt));
    });

    final toolCall = DemoAgentPlanner.plan(prompt);
    setState(() {
      _logs.insert(0, AgentLogEntry.toolCall(toolCall));
    });

    try {
      final result = await FlutterAppFunctions.instance.invoke(
        toolCall.functionId,
        toolCall.parameters,
      );
      setState(() {
        _logs.insert(0, AgentLogEntry.result(result));
      });
    } on AppFunctionException catch (error) {
      setState(() {
        _logs.insert(0, AgentLogEntry.error('${error.code}: ${error.message}'));
      });
    } finally {
      if (mounted) {
        setState(() => _running = false);
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: widget.store,
      builder: (context, _) {
        return Scaffold(
          appBar: AppBar(
            title: const Text('Agent AppFunctions MCP Hub'),
            actions: [
              Padding(
                padding: const EdgeInsets.only(right: 16),
                child: Center(
                  child: Text('${FlutterAppFunctions.instance.length} tools'),
                ),
              ),
            ],
          ),
          body: SafeArea(
            child: ListView(
              padding: const EdgeInsets.all(16),
              children: [
                const Text(
                  'AppFunctions background listener active',
                  style: TextStyle(fontWeight: FontWeight.w700),
                ),
                const SizedBox(height: 8),
                const Text(
                  'Real Gemini/AppFunctions callers discover these registered Dart handlers through Android. '
                  'This screen simulates the caller by choosing a function id and JSON parameters, then invoking the same registry.',
                ),
                const SizedBox(height: 16),
                _PromptPanel(
                  controller: _promptController,
                  running: _running,
                  onRun: _runSimulatedAgent,
                  onPromptSelected: (prompt) {
                    _promptController.text = prompt;
                  },
                ),
                const SizedBox(height: 16),
                _RegisteredToolsPanel(
                  definitions: FlutterAppFunctions.instance.definitions
                      .toList(),
                ),
                const SizedBox(height: 16),
                _StoreSnapshotPanel(store: widget.store),
                const SizedBox(height: 16),
                _AgentLogPanel(entries: _logs),
              ],
            ),
          ),
        );
      },
    );
  }
}

class _PromptPanel extends StatelessWidget {
  const _PromptPanel({
    required this.controller,
    required this.running,
    required this.onRun,
    required this.onPromptSelected,
  });

  final TextEditingController controller;
  final bool running;
  final VoidCallback onRun;
  final ValueChanged<String> onPromptSelected;

  @override
  Widget build(BuildContext context) {
    const samplePrompts = <String>[
      'Remind me to pick up my package at work today at 5 PM',
      'Find the noodle recipe from Lisa and add noodles, soy sauce, and scallions to my shopping list',
      'I finished task 1',
      'Summarize my day',
    ];

    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              'Local AI caller simulation',
              style: TextStyle(fontWeight: FontWeight.w700),
            ),
            const SizedBox(height: 12),
            TextField(
              controller: controller,
              minLines: 2,
              maxLines: 4,
              decoration: const InputDecoration(
                labelText: 'User prompt',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 12),
            Wrap(
              spacing: 8,
              runSpacing: 8,
              children: [
                for (final prompt in samplePrompts)
                  OutlinedButton(
                    onPressed: () => onPromptSelected(prompt),
                    child: Text(prompt, overflow: TextOverflow.ellipsis),
                  ),
              ],
            ),
            const SizedBox(height: 12),
            FilledButton.icon(
              onPressed: running ? null : onRun,
              icon: running
                  ? const SizedBox.square(
                      dimension: 18,
                      child: CircularProgressIndicator(strokeWidth: 2),
                    )
                  : const Icon(Icons.smart_toy_outlined),
              label: const Text('Ask simulated agent'),
            ),
          ],
        ),
      ),
    );
  }
}

class _RegisteredToolsPanel extends StatelessWidget {
  const _RegisteredToolsPanel({required this.definitions});

  final List<AppFunctionDefinition> definitions;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              'Registered AppFunctions tools',
              style: TextStyle(fontWeight: FontWeight.w700),
            ),
            const SizedBox(height: 8),
            for (final definition in definitions)
              ListTile(
                contentPadding: EdgeInsets.zero,
                title: Text(definition.id),
                subtitle: Text(_toolSignature(definition)),
              ),
          ],
        ),
      ),
    );
  }

  String _toolSignature(AppFunctionDefinition definition) {
    final params = definition.parameters
        .map((param) {
          final requiredMarker = param.required ? '' : '?';
          return '${param.name}: ${param.type.wireName}$requiredMarker';
        })
        .join(', ');
    return '$params -> ${definition.returnType}';
  }
}

class _StoreSnapshotPanel extends StatelessWidget {
  const _StoreSnapshotPanel({required this.store});

  final DemoProductivityStore store;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              'App state changed by tool calls',
              style: TextStyle(fontWeight: FontWeight.w700),
            ),
            const SizedBox(height: 8),
            const Text('Tasks'),
            if (store.tasks.isEmpty) const Text('No tasks yet.'),
            for (final task in store.tasks)
              CheckboxListTile(
                contentPadding: EdgeInsets.zero,
                value: task.completed,
                onChanged: null,
                title: Text(task.title),
                subtitle: Text(
                  [
                    if (task.dueDateTime != null) task.dueDateTime,
                    if (task.location != null) task.location,
                  ].join(' - '),
                ),
              ),
            const Divider(),
            const Text('Shopping list'),
            if (store.shoppingItems.isEmpty)
              const Text('No shopping items yet.'),
            for (final item in store.shoppingItems)
              ListTile(
                contentPadding: EdgeInsets.zero,
                leading: const Icon(Icons.shopping_basket_outlined),
                title: Text(item.label),
                subtitle: item.source == null
                    ? null
                    : Text('Source: ${item.source}'),
              ),
          ],
        ),
      ),
    );
  }
}

class _AgentLogPanel extends StatelessWidget {
  const _AgentLogPanel({required this.entries});

  final List<AgentLogEntry> entries;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              'Caller trace',
              style: TextStyle(fontWeight: FontWeight.w700),
            ),
            const SizedBox(height: 8),
            if (entries.isEmpty)
              const Text(
                'Run a prompt to see the function id and parameters an agent would send.',
              ),
            for (final entry in entries)
              ListTile(
                contentPadding: EdgeInsets.zero,
                leading: Icon(entry.icon),
                title: Text(entry.title),
                subtitle: Text(entry.body),
              ),
          ],
        ),
      ),
    );
  }
}

class DemoAgentPlanner {
  static ToolCall plan(String prompt) {
    final lower = prompt.toLowerCase();

    if (lower.contains('shopping') || lower.contains('recipe')) {
      final items = _extractShoppingItems(prompt);
      return ToolCall(
        functionId: 'addItemsToShoppingList',
        parameters: <String, dynamic>{
          'items': items,
          'source': lower.contains('lisa')
              ? 'Lisa recipe request'
              : 'User prompt',
        },
      );
    }

    if (lower.contains('finished') ||
        lower.contains('done') ||
        lower.contains('complete')) {
      return ToolCall(
        functionId: 'completeTask',
        parameters: <String, dynamic>{'taskId': _firstNumber(prompt) ?? 1},
      );
    }

    if (lower.contains('summarize') || lower.contains('summary')) {
      return const ToolCall(functionId: 'summarizeToday');
    }

    return ToolCall(
      functionId: 'createTask',
      parameters: <String, dynamic>{
        'title': _extractTaskTitle(prompt),
        if (lower.contains('5 pm') || lower.contains('5pm'))
          'dueDateTime': 'today 5 PM',
        if (lower.contains('work')) 'location': 'Work',
      },
    );
  }

  static List<String> _extractShoppingItems(String prompt) {
    final lower = prompt.toLowerCase();
    final knownItems = <String>[
      'noodles',
      'soy sauce',
      'scallions',
      'ginger',
      'garlic',
      'eggs',
    ];
    final extracted = knownItems.where(lower.contains).toList();
    return extracted.isEmpty ? <String>['noodles', 'soy sauce'] : extracted;
  }

  static String _extractTaskTitle(String prompt) {
    var title = prompt
        .replaceFirst(RegExp('remind me to ', caseSensitive: false), '')
        .replaceFirst(RegExp('please ', caseSensitive: false), '')
        .trim();
    if (title.isEmpty) title = 'Follow up from agent request';
    return title[0].toUpperCase() + title.substring(1);
  }

  static int? _firstNumber(String prompt) {
    final match = RegExp(r'\d+').firstMatch(prompt);
    return match == null ? null : int.tryParse(match.group(0)!);
  }
}

class DemoProductivityStore extends ChangeNotifier {
  final List<TaskItem> _tasks = <TaskItem>[];
  final List<ShoppingItem> _shoppingItems = <ShoppingItem>[];
  int _nextTaskId = 1;

  List<TaskItem> get tasks => List.unmodifiable(_tasks);
  List<ShoppingItem> get shoppingItems => List.unmodifiable(_shoppingItems);

  TaskItem createTask({
    required String title,
    String? dueDateTime,
    String? location,
  }) {
    final task = TaskItem(
      id: _nextTaskId++,
      title: title,
      dueDateTime: dueDateTime,
      location: location,
    );
    _tasks.add(task);
    notifyListeners();
    return task;
  }

  List<ShoppingItem> addShoppingItems({
    required List<String> items,
    String? source,
  }) {
    final added = items
        .map((item) => ShoppingItem(label: item.trim(), source: source))
        .where((item) => item.label.isNotEmpty)
        .toList();
    _shoppingItems.addAll(added);
    notifyListeners();
    return added;
  }

  TaskItem? completeTask(int taskId) {
    final index = _tasks.indexWhere((task) => task.id == taskId);
    if (index == -1) return null;
    final completed = _tasks[index].copyWith(completed: true);
    _tasks[index] = completed;
    notifyListeners();
    return completed;
  }

  String summaryForAgent() {
    final openTasks = _tasks.where((task) => !task.completed).length;
    final completedTasks = _tasks.where((task) => task.completed).length;
    return 'Open tasks: $openTasks. Completed tasks: $completedTasks. '
        'Shopping items: ${_shoppingItems.map((item) => item.label).join(', ')}.';
  }
}

class TaskItem {
  const TaskItem({
    required this.id,
    required this.title,
    this.dueDateTime,
    this.location,
    this.completed = false,
  });

  final int id;
  final String title;
  final String? dueDateTime;
  final String? location;
  final bool completed;

  TaskItem copyWith({bool? completed}) {
    return TaskItem(
      id: id,
      title: title,
      dueDateTime: dueDateTime,
      location: location,
      completed: completed ?? this.completed,
    );
  }

  Map<String, dynamic> toJson() => <String, dynamic>{
    'id': id,
    'title': title,
    if (dueDateTime != null) 'dueDateTime': dueDateTime,
    if (location != null) 'location': location,
    'completed': completed,
  };
}

class ShoppingItem {
  const ShoppingItem({required this.label, this.source});

  final String label;
  final String? source;
}

class ToolCall {
  const ToolCall({
    required this.functionId,
    this.parameters = const <String, dynamic>{},
  });

  final String functionId;
  final Map<String, dynamic> parameters;

  String toPrettyJson() {
    return const JsonEncoder.withIndent('  ').convert(<String, dynamic>{
      'functionId': functionId,
      'parametersJson': jsonEncode(parameters),
    });
  }
}

class AgentLogEntry {
  const AgentLogEntry._({
    required this.title,
    required this.body,
    required this.icon,
  });

  factory AgentLogEntry.user(String prompt) => AgentLogEntry._(
    title: 'User prompt',
    body: prompt,
    icon: Icons.person_outline,
  );

  factory AgentLogEntry.toolCall(ToolCall toolCall) => AgentLogEntry._(
    title: 'Agent selected AppFunction',
    body: toolCall.toPrettyJson(),
    icon: Icons.hub_outlined,
  );

  factory AgentLogEntry.result(Object? result) => AgentLogEntry._(
    title: 'AppFunction result',
    body: '$result',
    icon: Icons.check_circle_outline,
  );

  factory AgentLogEntry.error(String error) => AgentLogEntry._(
    title: 'AppFunction error',
    body: error,
    icon: Icons.error_outline,
  );

  final String title;
  final String body;
  final IconData icon;
}
5
likes
160
points
385
downloads

Documentation

API reference

Publisher

verified publisherbrainovatech.in

Weekly Downloads

Flutter bridge for Android App Functions. Register typed Dart functions that Android agents such as Gemini can discover and invoke on-device.

Repository (GitHub)
View/report issues

Topics

#appfunctions #android #ai #tools #gemini

License

MIT (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on flutter_app_functions

Packages that implement flutter_app_functions