Flutter Client

Official SDK v0.5.0 iOS 13+ / Android 8.0+

Getting Started

Installation

pub.dev (Recommended)

Add to your pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  solidb_flutter: ^0.5.0
  
dev_dependencies:
  build_runner: ^2.4.0
  solidb_flutter_generator: ^0.5.0

Then run:

flutter pub get

Platform-Specific Dependencies

The SDK automatically includes required native dependencies for offline storage.

Requirements: Flutter 3.16+, Dart 3.0+, iOS 13+, Android API 26+ (8.0+). The SDK uses FFI for high-performance native bindings.

Platform Setup

iOS Setup

Update ios/Podfile:

platform :ios, '13.0'

# Add to your target
target 'Runner' do
  use_frameworks!
  use_modular_headers!
  
  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
  
  # SoliDB requires these permissions for background sync
  pod 'Permission-LocationWhenInUse', '~> 1.0'
end

post_install do |installer|
  installer.pods_project.targets.each do |target|
    flutter_additional_ios_build_settings(target)
    
    # Enable FFI support
    target.build_configurations.each do |config|
      config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'
    end
  end
end

Update ios/Runner/Info.plist:

<key>NSAppTransportSecurity</key>
<dict>
  <key>NSAllowsArbitraryLoads</key>
  <false/>
  <key>NSExceptionDomains</key>
  <dict>
    <key>your-api-server.com</key>
    <dict>
      <key>NSIncludesSubdomains</key>
      <true/>
    </dict>
  </dict>
</dict>

<!-- Background sync (optional) -->
<key>UIBackgroundModes</key>
<array>
  <string>fetch</string>
</array>

Android Setup

Update android/app/src/main/AndroidManifest.xml:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />

<application
  android:label="Your App"
  android:name="${applicationName}"
  android:icon="@mipmap/ic_launcher">
  
  <!-- SoliDB sync service -->
  <service
    android:name="com.solidb.flutter.SyncService"
    android:foregroundServiceType="dataSync"
    android:exported="false" />
  
  <!-- ... rest of your application config -->
</application>

Update android/app/build.gradle:

android {
  compileSdkVersion 34
  
  defaultConfig {
    minSdkVersion 26  // Required for SoliDB
    targetSdkVersion 34
  }
  
  // Enable FFI support for native bindings
  externalNativeBuild {
    cmake {
      version "3.22.1"
    }
  }
}

Quick Start

import 'package:flutter/material.dart';
import 'package:solidb_flutter/solidb_flutter.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // Initialize SoliDB client
  final client = SoliDBClient(
    config: SoliDBConfig(
      host: 'api.example.com',
      port: 6745,
      database: 'mydb',
      username: 'admin',
      password: 'password',
      offlineMode: true,        // Enable offline-first
      syncInterval: Duration(seconds: 30),
    ),
  );
  
  await client.connect();
  
  runApp(MyApp(client: client));
}

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

  @override
  Widget build(BuildContext context) {
    return SoliDBProvider(
      client: client,
      child: MaterialApp(
        title: 'SoliDB Demo',
        home: TodoScreen(),
      ),
    );
  }
}

class TodoScreen extends StatefulWidget {
  @override
  State<TodoScreen> createState() => _TodoScreenState();
}

class _TodoScreenState extends State<TodoScreen> {
  List<Map<String, dynamic>> todos = [];

  @override
  void initState() {
    super.initState();
    _loadTodos();
  }

  Future<void> _loadTodos() async {
    final client = SoliDBProvider.of(context).client;
    final results = await client.query(
      'FOR t IN todos SORT t.created_at DESC RETURN t',
    );
    setState(() {
      todos = results;
    });
  }

  Future<void> _addTodo(String title) async {
    final client = SoliDBProvider.of(context).client;
    await client.save('todos', {
      'title': title,
      'completed': false,
      'created_at': DateTime.now().millisecondsSinceEpoch,
    });
    _loadTodos();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Todos')),
      body: ListView.builder(
        itemCount: todos.length,
        itemBuilder: (context, index) {
          final todo = todos[index];
          return ListTile(
            title: Text(todo['title']),
          );
        },
      ),
    );
  }
}

Client Configuration

import 'package:solidb_flutter/solidb_flutter.dart';

final client = SoliDBClient(
  config: SoliDBConfig(
    // Connection settings
    host: 'localhost',
    port: 6745,
    database: 'mydb',
    username: 'admin',
    password: 'password',
    timeout: Duration(seconds: 30),
    enableCompression: true,
    enableSSL: false,          // Set to true for production
    
    // Offline settings
    offlineMode: true,
    syncInterval: Duration(seconds: 30),
    syncOnConnectivityChange: true,
    
    // Storage configuration
    storage: StorageConfig(
      engine: StorageEngine.sqlite,  // or StorageEngine.asyncStorage
      encryptionKey: 'optional-key', // Encrypt local data
      maxCacheSize: 100 * 1024 * 1024, // 100 MB
    ),
    
    // Conflict resolution
    conflictResolution: ConflictResolution.lastWriteWins,
    // Options: firstWriteWins, serverWins, clientWins, custom()
    
    // Retry configuration
    retryAttempts: 5,
    retryDelay: Duration(seconds: 2),
  ),
);

// Connect with error handling
try {
  await client.connect();
  print('Connected to SoliDB');
} on SoliDBException catch (e) {
  print('Connection failed: \${e.message}');
}

// Listen for connection state
client.connectionStream.listen((state) {
  print('Connection state: \$state');
});

// Disconnect when done
@override
void dispose() {
  client.disconnect();
  super.dispose();
}
Property Type Default Description
hostStringlocalhostServer hostname or IP
portint6745Server port
timeoutDuration30sConnection timeout
offlineModeboolfalseEnable offline-first sync
syncIntervalDuration30sAuto-sync interval
storage.engineStorageEnginesqliteLocal storage engine

Core Operations

Collection Operations

// List collections in a database
final collections = await client.listCollections();
// => ['users', 'orders', 'products']

// Create a document collection
await client.createCollection('products');

// Create an edge collection (for graphs)
await client.createCollection(
  'relationships',
  type: CollectionType.edge,
);

// Delete a collection
await client.deleteCollection('old_collection');

// Get collection statistics
final stats = await client.collectionStats('users');
print('Document count: \${stats['document_count']}');
Method Returns Description
listCollections()Future<List<String>>List collections in database
createCollection(name, type)Future<void>Create collection (document or edge)
deleteCollection(name)Future<void>Delete collection
collectionStats(name)Future<Map>Get collection statistics

Document Operations (CRUD)

// SAVE - Create or update a document
final doc = await client.save('users', {
  'name': 'Alice',
  'email': '[email protected]',
  'age': 30,
  'tags': ['developer', 'flutter'],
});
print('Key: \${doc['_key']}');  // Auto-generated key

// SAVE with custom key
final docWithKey = await client.save(
  'users',
  {
    '_key': 'user-123',
    'name': 'Bob',
    'email': '[email protected]',
  },
);

// GET - Retrieve a document by key
final user = await client.get('users', 'user-123');
print('Name: \${user['name']}');

// DELETE - Remove a document
await client.delete('users', 'user-123');

// LIST - Paginated document listing
final result = await client.list(
  'users',
  limit: 50,
  offset: 0,
);
print('Showing \${result.docs.length} of \${result.total} documents');
Method Returns Description
save(collection, document)Future<Document>Insert document, returns with key
get(collection, key)Future<Document>Get document by key
delete(collection, key)Future<void>Delete document
list(collection, options)Future<ListResult>List documents with pagination

SDBQL Queries

// Simple query
final users = await client.query('FOR u IN users RETURN u');

// Query with bind variables (recommended for security)
final results = await client.query(
  '''FOR u IN users
      FILTER u.age >= @min_age AND u.status == @status
      SORT u.created_at DESC
      LIMIT @limit
      RETURN { name: u.name, email: u.email }''',
  bindVars: {
    'min_age': 18,
    'status': 'active',
    'limit': 100,
  },
);

// Aggregation query
final stats = await client.query(
  '''FOR u IN users
      COLLECT status = u.status WITH COUNT INTO count
      RETURN { status, count }''',
);

// Join query
final orders = await client.query(
  '''FOR o IN orders
      FOR u IN users FILTER u._key == o.user_id
      RETURN { order: o, user: u.name }''',
);

Stream-based Sync & Subscriptions

// Subscribe to live query updates
final subscription = client.sync
  .subscribeQuery("FOR u IN users FILTER u.status == 'online' RETURN u")
  .listen((docs) {
    print('Received \${docs.length} updates');
    // Update your UI
  }, onError: (error) {
    print('Sync error: \$error');
  });

// Subscribe to collection changes
final collectionSub = client.sync
  .subscribeCollection('users', filter: {'status': 'active'})
  .listen((change) {
    switch (change.type) {
      case ChangeType.insert:
        print('New document: \${change.key}');
        break;
      case ChangeType.update:
        print('Updated: \${change.key}');
        break;
      case ChangeType.delete:
        print('Deleted: \${change.key}');
        break;
    }
  });

// Cancel subscriptions when done (in dispose)
@override
void dispose() {
  subscription.cancel();
  collectionSub.cancel();
  super.dispose();
}

// Manual sync (pull latest changes)
await client.sync.pull();

// Push local changes immediately
await client.sync.push();

// Get sync status
final status = await client.sync.status;
print('Pending changes: \${status.pendingChanges}');
print('Last sync: \${status.lastSyncTime}');
Method Returns Description
sync.subscribeQuery(query)Stream<List>Subscribe to query results
sync.subscribeCollection(name, filter)Stream<Change>Subscribe to collection changes
sync.pull()Future<SyncResult>Pull latest changes from server
sync.push()Future<SyncResult>Push local changes to server

Widget API

SoliDB Flutter provides reactive widgets for declarative data binding. Build UI that automatically updates when data changes.

SoliDBBuilder

General-purpose builder widget for any SoliDB operation with loading, error, and data states.

SoliDBBuilder<List<Map>>(
  // Execute any SoliDB operation
  future: (client) => client.query(
    'FOR u IN users FILTER u.active == true RETURN u'
  ),
  
  // Customize loading state
  loading: (context) => const Center(
    child: CircularProgressIndicator(),
  ),
  
  // Customize error state
  error: (context, error, retry) => Center(
    child: Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Text('Error: \$error'),
        ElevatedButton(
          onPressed: retry,
          child: const Text('Retry'),
        ),
      ],
    ),
  ),
  
  // Build with data
  builder: (context, users) => ListView.builder(
    itemCount: users.length,
    itemBuilder: (context, index) {
      final user = users[index];
      return ListTile(
        title: Text(user['name']),
        subtitle: Text(user['email']),
      );
    },
  ),
)

SoliDBStreamBuilder

Real-time stream builder that automatically rebuilds when data changes. Perfect for live updates.

SoliDBStreamBuilder<List<Map>>(
  // Subscribe to real-time updates
  stream: (client) => client.sync
    .subscribeQuery('FOR u IN users RETURN u'),
  
  // Optional: Initial data while loading
  initialData: [],
  
  // Customize loading state
  loading: (context) => const Center(
    child: CircularProgressIndicator(),
  ),
  
  // Build with data (rebuilds automatically on updates)
  builder: (context, users) => ListView.builder(
    itemCount: users.length,
    itemBuilder: (context, index) {
      final user = users[index];
      return ListTile(
        title: Text(user['name']),
        trailing: Text('Active: \${user['active']}'),
      );
    },
  ),
)

SoliDBDocument

Specialized widget for single document operations with automatic updates.

SoliDBDocument(
  collection: 'users',
  documentId: 'user-123',
  
  // Enable real-time updates
  live: true,
  
  // Customize loading
  loading: (context) => const Center(
    child: CircularProgressIndicator(),
  ),
  
  // Customize not found
  notFound: (context) => const Center(
    child: Text('User not found'),
  ),
  
  // Build with document and update callback
  builder: (context, user, update) => Column(
    children: [
      Text(user['name'], style: Theme.of(context).textTheme.headlineSmall),
      Text(user['email']),
      Switch(
        value: user['active'],
        onChanged: (value) async {
          // Optimistic update
          await update({'active': value});
        },
      ),
    ],
  ),
)

Offline-First Sync

Offline-First Architecture

SoliDB Flutter provides automatic offline-first capabilities using SQLite. All operations work seamlessly offline and sync when connectivity is restored.

// Enable offline-first mode in configuration
final client = SoliDBClient(
  config: SoliDBConfig(
    host: 'api.example.com',
    port: 6745,
    database: 'mydb',
    username: 'admin',
    password: 'password',
    offlineMode: true,           // Enable offline support
    syncInterval: Duration(seconds: 30),
    
    // SQLite storage configuration
    storage: StorageConfig(
      engine: StorageEngine.sqlite,
      path: 'solidb_data',       // Database file name
      encryptionKey: 'optional', // Encrypt local data
      maxSize: 100 * 1024 * 1024, // 100 MB limit
    ),
    
    // Conflict resolution
    conflictResolution: ConflictResolution.lastWriteWins,
    // Options: firstWriteWins, serverWins, clientWins, custom()
    
    // Delta sync (only sync changed fields)
    enableDeltaSync: true,
    deltaSyncThreshold: 1024,    // Use delta for docs > 1KB
    
    // Network handling
    retryAttempts: 5,
    retryDelay: Duration(seconds: 2),
    syncOnConnectivityChange: true,
  ),
);

// All operations work offline
// Changes are automatically queued and synced
final doc = await client.save('notes', {
  'title': 'Important Note',
  'content': '...',
});
// Works even without network! Automatically syncs later.

// Listen for sync events
client.syncEvents.listen((event) {
  switch (event.type) {
    case SyncEventType.start:
      print('Sync started');
      break;
    case SyncEventType.complete:
      print('Synced: \${event.pushed} up, \${event.pulled} down');
      break;
    case SyncEventType.error:
      print('Sync failed: \${event.error}');
      break;
    case SyncEventType.conflict:
      print('Conflict resolved for: \${event.documentKey}');
      break;
  }
});
Instant Operations

All reads and writes happen immediately to local SQLite. Zero network latency.

SQLite Storage

Uses SQLite for robust offline data persistence with full query capabilities.

Auto Sync

Automatic background sync with configurable intervals. Respects battery and connectivity.

Learn More

For advanced offline sync features like conflict resolution strategies, delta sync, and selective sync, see the Offline Sync Documentation.

Examples

Complete Todo App

A fully functional offline-first Todo app using reactive widgets, real-time sync, and optimistic updates.

import 'package:flutter/material.dart';
import 'package:solidb_flutter/solidb_flutter.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  final client = SoliDBClient(
    config: SoliDBConfig(
      host: 'api.example.com',
      port: 6745,
      database: 'todoapp',
      username: 'user',
      password: 'pass',
      offlineMode: true,
      syncInterval: Duration(seconds: 10),
    ),
  );
  
  await client.connect();
  
  runApp(TodoApp(client: client));
}

class TodoApp extends StatelessWidget {
  final SoliDBClient client;
  
  const TodoApp({super.key, required this.client});

  @override
  Widget build(BuildContext context) {
    return SoliDBProvider(
      client: client,
      child: MaterialApp(
        title: 'SoliDB Todos',
        theme: ThemeData(
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
          useMaterial3: true,
        ),
        home: const TodoScreen(),
      ),
    );
  }
}

class TodoScreen extends StatefulWidget {
  const TodoScreen({super.key});

  @override
  State<TodoScreen> createState() => _TodoScreenState();
}

class _TodoScreenState extends State<TodoScreen> {
  final _textController = TextEditingController();

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

  Future<void> _addTodo(SoliDBClient client, String title) async {
    if (title.trim().isEmpty) return;
    
    await client.save('todos', {
      'title': title.trim(),
      'completed': false,
      'created_at': DateTime.now().millisecondsSinceEpoch,
    });
    
    _textController.clear();
  }

  Future<void> _toggleTodo(
    SoliDBClient client,
    String key,
    bool completed,
  ) async {
    await client.save('todos', {
      '_key': key,
      'completed': !completed,
    });
  }

  Future<void> _deleteTodo(SoliDBClient client, String key) async {
    await client.delete('todos', key);
  }

  @override
  Widget build(BuildContext context) {
    final client = SoliDBProvider.of(context).client;
    
    return Scaffold(
      appBar: AppBar(
        title: const Text('My Todos'),
        actions: [
          // Sync status indicator
          StreamBuilder<SyncStatus>(
            stream: client.syncStatus,
            builder: (context, snapshot) {
              final status = snapshot.data;
              return Row(
                children: [
                  if (status?.isSyncing == true)
                    const SizedBox(
                      width: 20,
                      height: 20,
                      child: CircularProgressIndicator(strokeWidth: 2),
                    ),
                  if (status?.pendingChanges != null && 
                      status!.pendingChanges > 0)
                    Badge(
                      label: Text('${status.pendingChanges}'),
                      child: const Icon(Icons.pending),
                    ),
                  IconButton(
                    icon: Icon(
                      status?.isConnected == true
                        ? Icons.cloud_done
                        : Icons.cloud_off,
                    ),
                    onPressed: () => client.sync.push(),
                  ),
                ],
              );
            },
          ),
        ],
      ),
      body: Column(
        children: [
          // Add todo input
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _textController,
                    decoration: const InputDecoration(
                      hintText: 'Add new todo...',
                      border: OutlineInputBorder(),
                    ),
                    onSubmitted: (value) => _addTodo(client, value),
                  ),
                ),
                const SizedBox(width: 8),
                IconButton(
                  icon: const Icon(Icons.add),
                  onPressed: () => _addTodo(client, _textController.text),
                ),
              ],
            ),
          ),
          
          // Real-time todo list
          Expanded(
            child: SoliDBStreamBuilder<List<Map>>(
              stream: (c) => c.sync.subscribeQuery(
                'FOR t IN todos SORT t.created_at DESC RETURN t',
              ),
              loading: (_) => const Center(
                child: CircularProgressIndicator(),
              ),
              builder: (context, todos) => ListView.builder(
                itemCount: todos.length,
                itemBuilder: (context, index) {
                  final todo = todos[index];
                  return TodoItem(
                    todo: todo,
                    onToggle: () => _toggleTodo(
                      client,
                      todo['_key'],
                      todo['completed'],
                    ),
                    onDelete: () => _deleteTodo(client, todo['_key']),
                  );
                },
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class TodoItem extends StatelessWidget {
  final Map<String, dynamic> todo;
  final VoidCallback onToggle;
  final VoidCallback onDelete;

  const TodoItem({
    super.key,
    required this.todo,
    required this.onToggle,
    required this.onDelete,
  });

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: Checkbox(
        value: todo['completed'] ?? false,
        onChanged: (_) => onToggle(),
      ),
      title: Text(
        todo['title'],
        style: TextStyle(
          decoration: (todo['completed'] ?? false)
            ? TextDecoration.lineThrough
            : null,
          color: (todo['completed'] ?? false) ? Colors.grey : null,
        ),
      ),
      trailing: IconButton(
        icon: const Icon(Icons.delete, color: Colors.red),
        onPressed: onDelete,
      ),
    );
  }
}

Stream-based Sync Patterns

// Pattern 1: Real-time list with pagination
class PaginatedTodos extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final client = SoliDBProvider.of(context).client;
    
    return StreamBuilder<List<Map>>(
      stream: client.sync.subscribeQuery(
        'FOR t IN todos SORT t.created_at DESC LIMIT 20 RETURN t',
      ),
      builder: (context, snapshot) {
        if (snapshot.hasError) {
          return Text('Error: \${snapshot.error}');
        }
        
        if (!snapshot.hasData) {
          return const CircularProgressIndicator();
        }
        
        return ListView.builder(
          itemCount: snapshot.data!.length,
          itemBuilder: (context, index) {
            return TodoTile(todo: snapshot.data![index]);
          },
        );
      },
    );
  }
}

// Pattern 2: Collection change events
class ChangeNotifier extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final client = SoliDBProvider.of(context).client;
    
    return StreamBuilder<CollectionChange>(
      stream: client.sync.subscribeCollection('notifications'),
      builder: (context, snapshot) {
        if (snapshot.hasData) {
          final change = snapshot.data!;
          
          // Show snackbar for new notifications
          if (change.type == ChangeType.insert) {
            WidgetsBinding.instance.addPostFrameCallback((_) {
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(
                  content: Text('New: \${change.document['message']}'),
                ),
              );
            });
          }
        }
        
        return Container(); // Or your actual UI
      },
    );
  }
}

// Pattern 3: Combine multiple streams
class Dashboard extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final client = SoliDBProvider.of(context).client;
    
    return StreamBuilder<Map<String, dynamic>>(
      stream: Rx.combineLatest3(
        client.sync.subscribeQuery(
          'FOR t IN todos FILTER !t.completed RETURN t',
        ),
        client.sync.subscribeQuery(
          'FOR u IN users FILTER u.online RETURN u',
        ),
        client.sync.subscribeCollection('orders'),
        (todos, users, orders) => {
          'pendingTodos': todos,
          'onlineUsers': users,
          'recentOrders': orders,
        },
      ),
      builder: (context, snapshot) {
        if (!snapshot.hasData) return const CircularProgressIndicator();
        
        return Column(
          children: [
            StatsCard(
              title: 'Pending Todos',
              count: snapshot.data!['pendingTodos'].length,
            ),
            StatsCard(
              title: 'Online Users',
              count: snapshot.data!['onlineUsers'].length,
            ),
            // ...
          ],
        );
      },
    );
  }
}

Error Handling

import 'package:solidb_flutter/solidb_flutter.dart';

try {
  await client.connect();
} on SoliDBConnectionException catch (e) {
  print('Connection failed: \${e.message}');
  // Will auto-retry in offline mode
} on SoliDBAuthenticationException catch (e) {
  print('Authentication failed: \${e.message}');
  // Handle invalid credentials
} on SoliDBDocumentNotFoundException catch (e) {
  print('Document not found: \${e.key}');
} on SoliDBNetworkException catch (e) {
  print('Network error: \${e.message}');
  // Offline mode will queue for retry
} on SoliDBSyncConflictException catch (e) {
  print('Sync conflict: \${e.message}');
  // Conflict was resolved automatically
} on SoliDBException catch (e) {
  print('SoliDB error [\${e.code}]: \${e.message}');
} catch (e) {
  print('Unexpected error: \$e');
}

// Error handling in widgets
SoliDBBuilder<List>(
  future: (client) => client.query('FOR u IN users RETURN u'),
  error: (context, error, retry) {
    String message = 'An error occurred';
    
    if (error is SoliDBNetworkException) {
      message = 'No internet connection. Working offline.';
    } else if (error is SoliDBAuthenticationException) {
      message = 'Please log in again.';
    }
    
    return Center(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Icon(Icons.error_outline, color: Colors.red, size: 48),
          SizedBox(height: 16),
          Text(message),
          SizedBox(height: 16),
          ElevatedButton(
            onPressed: retry,
            child: Text('Retry'),
          ),
        ],
      ),
    );
  },
  builder: (context, data) => ListView(...),
)