Flutter Client
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 |
|---|---|---|---|
host | String | localhost | Server hostname or IP |
port | int | 6745 | Server port |
timeout | Duration | 30s | Connection timeout |
offlineMode | bool | false | Enable offline-first sync |
syncInterval | Duration | 30s | Auto-sync interval |
storage.engine | StorageEngine | sqlite | Local 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;
}
});
All reads and writes happen immediately to local SQLite. Zero network latency.
Uses SQLite for robust offline data persistence with full query capabilities.
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(...),
)
Next Steps
Explore more features of the SoliDB Flutter SDK: