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