Flutter Integration
Build mobile apps with EdgeBase as the backend.
Setup
dart pub add edgebase_flutter
The edgebase_flutter package includes platform-specific optimizations:
- Default refresh-token persistence via
shared_preferences, or a customTokenStorage - Automatic token refresh on app launch and background-to-foreground transitions
- WebSocket reconnection with exponential backoff for realtime subscriptions
Initialization
Create a single client instance and reuse it throughout your app. A common pattern is to initialize it in main.dart or a dependency injection setup:
import 'package:edgebase_flutter/edgebase_flutter.dart';
// Create once, use everywhere
final client = ClientEdgeBase('https://your-project.edgebase.fun');
Avoid creating multiple ClientEdgeBase instances — each one opens its own WebSocket connection and manages its own auth state. Use a single instance and pass it via InheritedWidget, Provider, or any DI approach your app uses.
Authentication
// Sign up
await client.auth.signUp(SignUpOptions(email: 'user@example.com', password: 'password'));
// Sign in
await client.auth.signIn(
SignInOptions(email: 'user@example.com', password: 'password'),
);
// Sign out
await client.auth.signOut();
// Get current user (null if not signed in)
final user = client.auth.currentUser;
Auth State Listener
Use onAuthStateChange to reactively navigate between login and home screens. This fires on sign-in, sign-out, and token refresh:
client.auth.onAuthStateChange.listen((user) {
if (user != null) {
// Navigate to home
} else {
// Navigate to login
}
});
If you set up auth state listeners in a StatefulWidget, unsubscribe in dispose() to avoid memory leaks and setState() calls on unmounted widgets:
late final StreamSubscription<TokenUser?> _authSub;
void initState() {
super.initState();
_authSub = client.auth.onAuthStateChange.listen((user) {
setState(() { /* update UI */ });
});
}
void dispose() {
_authSub.cancel();
super.dispose();
}
Database
The database API is the same across all SDKs. Use client.db() for client-side access (respects access rules):
// Create
final post = await client.db('app').table('posts').insert({
'title': 'Hello from Flutter!',
'content': 'My mobile post.',
});
// Query
final posts = await client.db('app').table('posts')
.where('status', '==', 'published')
.orderBy('createdAt', direction: 'desc')
.limit(20)
.getList();
// Update
await client.db('app').table('posts').update(post['id'], {
'title': 'Updated title',
});
// Delete
await client.db('app').table('posts').delete(post['id']);
File Upload
Upload files with progress tracking — useful for showing a progress bar in the UI:
// Pick and upload an image
final picker = ImagePicker();
final image = await picker.pickImage(source: ImageSource.gallery);
final bytes = await image!.readAsBytes();
await client.storage.bucket('avatars').upload(
'${client.auth.currentUser!.id}.jpg',
bytes,
contentType: 'image/jpeg',
onProgress: (percent) => setState(() => _progress = percent),
);
Subscriptions
DB Subscriptions
Listen to table changes in real time. Always unsubscribe in dispose() to close the WebSocket listener:
class LivePostsWidget extends StatefulWidget {
_LivePostsWidgetState createState() => _LivePostsWidgetState();
}
class _LivePostsWidgetState extends State<LivePostsWidget> {
final List<Map<String, dynamic>> _posts = [];
StreamSubscription<DbChange>? _sub;
void initState() {
super.initState();
_sub = client.db('app').table('posts').onSnapshot().listen((change) {
setState(() {
switch (change.changeType) {
case 'create':
_posts.add(change.data!);
break;
case 'update':
final idx = _posts.indexWhere((p) => p['id'] == change.docId);
if (idx >= 0) _posts[idx] = change.data!;
break;
case 'delete':
_posts.removeWhere((p) => p['id'] == change.docId);
break;
}
});
});
}
void dispose() {
_sub?.cancel();
super.dispose();
}
Widget build(BuildContext context) {
return ListView.builder(
itemCount: _posts.length,
itemBuilder: (ctx, i) => ListTile(title: Text(_posts[i]['title'])),
);
}
}
Room Members (Presence)
Track and display online users using Room members. Same pattern — subscribe in initState, clean up in dispose:
class OnlineUsersWidget extends StatefulWidget {
_OnlineUsersWidgetState createState() => _OnlineUsersWidgetState();
}
class _OnlineUsersWidgetState extends State<OnlineUsersWidget> {
List<Map<String, dynamic>> _users = [];
late final RoomClient room;
RoomSubscription? _joinSub;
RoomSubscription? _leaveSub;
void initState() {
super.initState();
room = client.room('presence', 'online-users');
room.join().then((_) async {
await room.members.setState({'name': 'Jane', 'status': 'active'});
setState(() => _users = room.members.list());
});
_joinSub = room.members.onJoin((member) {
setState(() => _users = room.members.list());
});
_leaveSub = room.members.onLeave((member) {
setState(() => _users = room.members.list());
});
}
void dispose() {
_joinSub?.cancel();
_leaveSub?.cancel();
room.leave();
super.dispose();
}
Widget build(BuildContext context) {
return Text('${_users.length} online');
}
}
Flutter Lifecycle Patterns
The dispose() Rule
Every subscription or listener created in initState() must be cleaned up in dispose(). This applies to:
| Resource | Subscribe | Clean up |
|---|---|---|
| Auth state | client.auth.onAuthStateChange.listen(...) | Cancel the StreamSubscription |
| DB snapshot | client.db(...).table(...).onSnapshot().listen(...) | Cancel the StreamSubscription |
| Room membership | room.members.onJoin(...) / room.members.onLeave(...) | Cancel the returned RoomSubscription and call room.leave() |
Forgetting to dispose will cause memory leaks and setState() called after dispose() errors.
App Lifecycle
The SDK automatically handles:
- Token refresh when the app returns from background
- WebSocket reconnection with exponential backoff after network interruption
- Refresh-token persistence across app restarts via the default
shared_preferencesstorage or a customTokenStorage
You don't need to manually manage reconnection or token storage.
Offline Support
The Flutter SDK persists the refresh token by default and refreshes access tokens automatically on app launch, so users stay signed in across app restarts without re-entering credentials.
- If you need stronger guarantees, provide a custom
TokenStorage - A custom storage adapter is often a good choice when you already standardize on your own secure storage layer