Flutter Integration
Build AI-powered Flutter apps with the Fig1 SDK.
Setup
Add http and flutter_riverpod (or your preferred state management):
# pubspec.yaml
dependencies:
http: ^1.1.0
flutter_riverpod: ^2.4.0
API Client
// lib/services/fig1_client.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
class Fig1Client {
static const String _baseUrl = 'https://app.fig1.ai/api/sdk';
final String apiKey;
Fig1Client({required this.apiKey});
Future<Map<String, dynamic>> _request(
String endpoint, {
String method = 'GET',
Map<String, dynamic>? body,
}) async {
final uri = Uri.parse('$_baseUrl$endpoint');
final headers = {
'Content-Type': 'application/json',
'X-Fig1-API-Key': apiKey,
};
http.Response response;
switch (method) {
case 'POST':
response = await http.post(uri, headers: headers, body: jsonEncode(body));
break;
case 'PUT':
response = await http.put(uri, headers: headers, body: jsonEncode(body));
break;
case 'DELETE':
response = await http.delete(uri, headers: headers);
break;
default:
response = await http.get(uri, headers: headers);
}
final data = jsonDecode(response.body);
if (data['success'] != true) {
throw Exception(data['error'] ?? 'API request failed');
}
return data['data'];
}
Future<ChatResponse> chat(
String message, {
String? sessionId,
String? personaId,
Map<String, dynamic>? preferences,
}) async {
final data = await _request('/agent/chat', method: 'POST', body: {
'message': message,
if (sessionId != null) 'sessionId': sessionId,
if (personaId != null) 'personaId': personaId,
if (preferences != null) 'preferences': preferences,
});
return ChatResponse.fromJson(data);
}
Future<SearchResponse> search(String query, {int? maxResults}) async {
final data = await _request('/rag/search', method: 'POST', body: {
'query': query,
if (maxResults != null) 'maxResults': maxResults,
});
return SearchResponse.fromJson(data);
}
}
Models
// lib/models/chat_response.dart
class ChatResponse {
final String message;
final String sessionId;
final List<ClientAction>? actions;
ChatResponse({
required this.message,
required this.sessionId,
this.actions,
});
factory ChatResponse.fromJson(Map<String, dynamic> json) {
return ChatResponse(
message: json['message'],
sessionId: json['sessionId'],
actions: json['actions'] != null
? (json['actions'] as List).map((a) => ClientAction.fromJson(a)).toList()
: null,
);
}
}
class ClientAction {
final String type;
final Map<String, dynamic> payload;
final bool immediate;
ClientAction({
required this.type,
required this.payload,
this.immediate = true,
});
factory ClientAction.fromJson(Map<String, dynamic> json) {
return ClientAction(
type: json['type'],
payload: json['payload'] ?? {},
immediate: json['immediate'] ?? true,
);
}
}
class SearchResponse {
final List<SearchResult> results;
final int totalResults;
SearchResponse({required this.results, required this.totalResults});
factory SearchResponse.fromJson(Map<String, dynamic> json) {
return SearchResponse(
results: (json['results'] as List).map((r) => SearchResult.fromJson(r)).toList(),
totalResults: json['totalResults'],
);
}
}
class SearchResult {
final String id;
final String content;
final double score;
SearchResult({required this.id, required this.content, required this.score});
factory SearchResult.fromJson(Map<String, dynamic> json) {
return SearchResult(
id: json['id'],
content: json['content'],
score: (json['score'] as num).toDouble(),
);
}
}
Chat State with Riverpod
// lib/providers/chat_provider.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
class Message {
final String id;
final String role;
final String content;
Message({required this.id, required this.role, required this.content});
}
class ChatState {
final List<Message> messages;
final String? sessionId;
final bool isLoading;
final String? error;
ChatState({
this.messages = const [],
this.sessionId,
this.isLoading = false,
this.error,
});
ChatState copyWith({
List<Message>? messages,
String? sessionId,
bool? isLoading,
String? error,
}) {
return ChatState(
messages: messages ?? this.messages,
sessionId: sessionId ?? this.sessionId,
isLoading: isLoading ?? this.isLoading,
error: error,
);
}
}
class ChatNotifier extends StateNotifier<ChatState> {
final Fig1Client _client;
final String? personaId;
ChatNotifier(this._client, {this.personaId}) : super(ChatState());
Future<void> sendMessage(String text) async {
// Add user message
final userMessage = Message(
id: DateTime.now().millisecondsSinceEpoch.toString(),
role: 'user',
content: text,
);
state = state.copyWith(
messages: [...state.messages, userMessage],
isLoading: true,
error: null,
);
try {
final response = await _client.chat(
text,
sessionId: state.sessionId,
personaId: personaId,
);
final assistantMessage = Message(
id: (DateTime.now().millisecondsSinceEpoch + 1).toString(),
role: 'assistant',
content: response.message,
);
state = state.copyWith(
messages: [...state.messages, assistantMessage],
sessionId: response.sessionId,
isLoading: false,
);
// Handle actions
if (response.actions != null) {
_handleActions(response.actions!);
}
} catch (e) {
state = state.copyWith(
isLoading: false,
error: e.toString(),
);
}
}
void _handleActions(List<ClientAction> actions) {
for (final action in actions) {
if (!action.immediate) continue;
// Handle actions - navigation, etc.
}
}
void clearChat() {
state = ChatState();
}
}
// Provider
final fig1ClientProvider = Provider((ref) => Fig1Client(
apiKey: const String.fromEnvironment('FIG1_API_KEY'),
));
final chatProvider = StateNotifierProvider<ChatNotifier, ChatState>((ref) {
final client = ref.watch(fig1ClientProvider);
return ChatNotifier(client);
});
Chat Screen
// lib/screens/chat_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class ChatScreen extends ConsumerStatefulWidget {
const ChatScreen({super.key});
@override
ConsumerState<ChatScreen> createState() => _ChatScreenState();
}
class _ChatScreenState extends ConsumerState<ChatScreen> {
final _controller = TextEditingController();
final _scrollController = ScrollController();
@override
Widget build(BuildContext context) {
final chatState = ref.watch(chatProvider);
return Scaffold(
appBar: AppBar(title: const Text('Chat')),
body: Column(
children: [
Expanded(
child: ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.all(16),
itemCount: chatState.messages.length,
itemBuilder: (context, index) {
final message = chatState.messages[index];
final isUser = message.role == 'user';
return Align(
alignment: isUser ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.8,
),
decoration: BoxDecoration(
color: isUser ? Colors.blue : Colors.grey[200],
borderRadius: BorderRadius.circular(16),
),
child: Text(
message.content,
style: TextStyle(
color: isUser ? Colors.white : Colors.black,
),
),
),
);
},
),
),
if (chatState.isLoading)
const Padding(
padding: EdgeInsets.all(8),
child: Text('Thinking...'),
),
Padding(
padding: const EdgeInsets.all(8),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: InputDecoration(
hintText: 'Type a message...',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(24),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
onSubmitted: (_) => _sendMessage(),
),
),
const SizedBox(width: 8),
IconButton.filled(
onPressed: chatState.isLoading ? null : _sendMessage,
icon: const Icon(Icons.send),
),
],
),
),
],
),
);
}
void _sendMessage() {
final text = _controller.text.trim();
if (text.isEmpty) return;
_controller.clear();
ref.read(chatProvider.notifier).sendMessage(text);
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
});
}
@override
void dispose() {
_controller.dispose();
_scrollController.dispose();
super.dispose();
}
}
Action Handling with GoRouter
// lib/services/action_handler.dart
import 'package:go_router/go_router.dart';
class Fig1ActionHandler {
final GoRouter router;
Fig1ActionHandler(this.router);
void handleActions(List<ClientAction> actions, BuildContext context) {
for (final action in actions) {
if (!action.immediate) continue;
switch (action.type) {
case 'navigate':
final route = action.payload['route'] as String?;
if (route != null) {
context.push(route);
}
break;
case 'show_product':
final productId = action.payload['productId'] as String?;
if (productId != null) {
context.push('/products/$productId');
}
break;
}
}
}
}
Security
For production, use a backend proxy. Configure your API key with --dart-define:
flutter run --dart-define=FIG1_API_KEY=fig1_sdk_xxx