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.type) {
case ChangeType.create:
_posts.add(change.record!);
break;
case ChangeType.update:
final idx = _posts.indexWhere((p) => p['id'] == change.id);
if (idx >= 0) _posts[idx] = change.record!;
break;
case ChangeType.delete:
_posts.removeWhere((p) => p['id'] == change.id);
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