Skip to main content

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 custom TokenStorage
  • 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');
Global Singleton

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
}
});
Dispose Listeners

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:

ResourceSubscribeClean up
Auth stateclient.auth.onAuthStateChange.listen(...)Cancel the StreamSubscription
DB snapshotclient.db(...).table(...).onSnapshot().listen(...)Cancel the StreamSubscription
Room membershiproom.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_preferences storage or a custom TokenStorage

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.

Platform Notes
  • 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