Flutter Gemma Skill Development
Guide for wrapping flutter_gemma function calling capabilities into reusable, production-ready AI Skills.
What is a Skill?
A Skill is a self-contained AI capability package containing:
- System Instruction - How the model should behave
- Function Schema - JSON Schema for structured output
- Result Parser - Converts model output to typed results
- Prompt Builder - Constructs user input into model prompts
Skill vs Direct API Calls
// ❌ Direct calls - repetitive boilerplate
final session = await model.createSession(tools: [myTool]);
await session.addQueryChunk(Message.text(text: 'translate to French', isUser: true));
// ... parse JSON manually ...
// ✅ Using Skill - clean, reusable
final translationSkill = TranslationSkill();
final result = await translationSkill.execute(model, text: 'Hello');
Skill Architecture
Follow feature-based project structure for clean separation of concerns:
my_ai_skills/
├── lib/
│ ├── skill.dart # Skill base class & interfaces
│ ├── skill_result.dart # Result types (immutable)
│ ├── skill_engine.dart # Execution engine
│ └── features/
│ ├── translation/
│ │ ├── translation_skill.dart
│ │ ├── translation_schema.dart
│ │ └── translation_result.dart
│ └── summarization/
│ ├── summarization_skill.dart
│ ├── summarization_schema.dart
│ └── summarization_result.dart
Defining a Skill
Step 1: Define Result Types
// skill_result.dart
abstract class SkillResult {
bool get success;
String? get errorMessage;
}
class TranslationResult extends SkillResult {
final String translatedText;
final String sourceLanguage;
final String targetLanguage;
TranslationResult.success({
required this.translatedText,
required this.sourceLanguage,
required this.targetLanguage,
}) : success = true, errorMessage = null;
TranslationResult.error(String message)
: success = false,
errorMessage = message,
translatedText = '',
sourceLanguage = '',
targetLanguage = '';
}
class SummaryResult extends SkillResult {
final String summary;
final int wordCount;
final List<String>? keyPoints;
SummaryResult.success({
required this.summary,
required this.wordCount,
this.keyPoints,
}) : success = true, errorMessage = null;
SummaryResult.error(String message)
: success = false,
errorMessage = message,
summary = '',
wordCount = 0,
keyPoints = null;
}
Step 2: Define Function Schema
// schemas/translation_schema.dart
const translationFunctionSchema = {
'name': 'translate_text',
'description': 'Translate text between languages accurately',
'parameters': {
'type': 'object',
'properties': {
'translated_text': {
'type': 'string',
'description': 'The translated text',
},
'source_language': {
'type': 'string',
'description': 'Source language name or code',
},
'target_language': {
'type': 'string',
'description': 'Target language name or code',
},
},
'required': ['translated_text', 'source_language', 'target_language'],
},
};
Step 3: Implement Skill Class
// skills/translation_skill.dart
import 'dart:convert';
import 'package:flutter_gemma/flutter_gemma.dart';
import '../skill.dart';
import '../skill_result.dart';
import '../schemas/translation_schema.dart';
class TranslationSkill extends Skill {
@override
String get id => 'translation';
@override
String get name => 'Translator';
@override
String get description => 'Translate text between 140+ languages';
@override
String get systemInstruction => '''
You are an expert translator. Translate text accurately while preserving:
- Meaning and nuance
- Cultural context
- Technical terminology
- Tone and style
''';
@override
Map<String, dynamic>? get functionSchema => translationFunctionSchema;
@override
String buildPrompt(SkillInput input) {
final targetLang = input.extra?['target_language'] ?? 'English';
final sourceLang = input.extra?['source_language'];
final prefix = sourceLang != null
? 'Translate from $sourceLang to $targetLang:'
: 'Translate to $targetLang:';
return '$prefix\n\n${input.text}\n\nUse the translate_text function.';
}
@override
SkillResult parseResult(FunctionCall functionCall) {
try {
final args = jsonDecode(functionCall.argumentsText) as Map<String, dynamic>;
return TranslationResult.success(
translatedText: args['translated_text'] as String,
sourceLanguage: args['source_language'] as String,
targetLanguage: args['target_language'] as String,
);
} catch (e) {
return TranslationResult.error('Parse error: $e');
}
}
}
Step 4: Create SkillEngine
// skill_engine.dart
import 'dart:convert';
import 'package:flutter_gemma/flutter_gemma.dart';
import 'skill.dart';
import 'skill_result.dart';
class SkillEngine {
InferenceModel? _model;
bool get isModelLoaded => _model != null;
/// Execute a skill
Future<SkillResult> execute({
required Skill skill,
required String input,
Map<String, dynamic>? extra,
}) async {
if (_model == null) {
throw StateError('Model not loaded');
}
final tools = skill.functionSchema != null
? [Tool.fromJsonSchema(skill.functionSchema!)]
: null;
final session = await _model!.createSession(
systemInstruction: SystemInstruction.text(skill.systemInstruction),
tools: tools,
);
try {
await session.addQueryChunk(Message.text(
text: skill.buildPrompt(SkillInput(text: input, extra: extra)),
isUser: true,
));
final stream = session.getResponseStream();
FunctionCall? functionCall;
await for (final event in stream) {
if (event is FunctionCallResponse) {
functionCall = event.functionCall;
break;
} else if (event is ErrorResponse) {
throw Exception(event.message);
}
}
if (functionCall != null) {
return skill.parseResult(functionCall);
}
throw Exception('No function call returned');
} finally {
await session.close();
}
}
/// Execute with streaming (for real-time token display)
Stream<String> executeStream({
required Skill skill,
required String input,
}) async* {
final session = await _model!.createSession(
systemInstruction: SystemInstruction.text(skill.systemInstruction),
);
await session.addQueryChunk(Message.text(text: input, isUser: true));
final stream = session.getResponseStream();
await for (final event in stream) {
if (event is TextResponse) {
yield event.text;
}
}
}
}
Step 5: Skill Base Class
// skill.dart
import 'package:flutter/foundation.dart';
import 'package:flutter_gemma/flutter_gemma.dart';
abstract class Skill {
String get id;
String get name;
String get description;
String get systemInstruction;
Map<String, dynamic>? get functionSchema => null;
SkillResult parseResult(FunctionCall functionCall);
String buildPrompt(SkillInput input);
}
class SkillInput {
final String text;
final List<Uint8List>? images;
final Map<String, dynamic>? extra;
const SkillInput({
required this.text,
this.images,
this.extra,
});
}
Usage Example
This follows Flutter best practices with Riverpod and feature-based structure.
Project Structure
lib/
├── main.dart
├── app.dart
├── features/
│ └── translation/
│ ├── data/
│ │ └── translation_repository.dart
│ ├── domain/
│ │ └── translation_service.dart
│ └── presentation/
│ ├── screens/
│