Documentation Index
Fetch the complete documentation index at: https://docs.dynamic.xyz/docs/llms.txt
Use this file to discover all available pages before exploring further.
This guide will walk you through implementing session management in your Flutter app using the Dynamic Flutter SDK. You’ll learn how to manage authentication state, handle reactive UI updates with Streams, and create a seamless user experience.
Overview
Session management is a crucial part of any Web3 app. The Dynamic Flutter SDK provides powerful Stream-based reactive state management for user sessions, authentication state, and wallet updates. This guide covers the practical implementation patterns you’ll need to build a robust session management system.
Key Concepts
Reactive State with Streams
The SDK provides Dart Streams that automatically emit updates when state changes:
authenticatedUserChanges - Emits when user logs in or out
tokenChanges - Emits when the auth token changes
userWalletsChanges - Emits when wallets are created or updated
readyChanges - Emits when SDK initialization state changes
Automatic UI Updates
By subscribing to these streams in your Flutter widgets using StreamBuilder, the UI automatically updates when:
- Users log in or out
- Authentication tokens refresh
- Wallets are connected or created
- SDK ready state changes
Implementation Patterns
1. Basic Session Management
Start with a simple session management setup:
import 'package:dynamic_sdk/dynamic_sdk.dart';
import 'package:flutter/material.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
// Initialize SDK at app launch
DynamicSDK.init(
props: ClientProps(
environmentId: 'your-environment-id',
appLogoUrl: 'https://your-app.com/logo.png',
appName: 'Your App',
redirectUrl: 'yourapp://',
),
);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Dynamic App',
home: Stack(
children: [
const SessionManagedApp(),
// Dynamic SDK widget overlay
DynamicSDK.instance.dynamicWidget,
],
),
);
}
}
class SessionManagedApp extends StatelessWidget {
const SessionManagedApp({super.key});
@override
Widget build(BuildContext context) {
return StreamBuilder<bool?>(
stream: DynamicSDK.instance.sdk.readyChanges,
builder: (context, readySnapshot) {
final sdkReady = readySnapshot.data ?? false;
if (!sdkReady) {
return const LoadingView();
}
return StreamBuilder<String?>(
stream: DynamicSDK.instance.auth.tokenChanges,
builder: (context, tokenSnapshot) {
final isAuthenticated = tokenSnapshot.data != null;
if (isAuthenticated) {
return const MainAppView();
} else {
return const LoginView();
}
},
);
},
);
}
}
class LoadingView extends StatelessWidget {
const LoadingView({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
}
class LoginView extends StatelessWidget {
const LoginView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () {
DynamicSDK.instance.ui.showAuth();
},
child: const Text('Sign In with Dynamic'),
),
),
);
}
}
class MainAppView extends StatelessWidget {
const MainAppView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('My App'),
actions: [
IconButton(
icon: const Icon(Icons.account_circle),
onPressed: () {
DynamicSDK.instance.ui.showUserProfile();
},
),
],
),
body: const Center(
child: Text('Welcome to your app!'),
),
);
}
}
2. Advanced Session State Management
For more complex apps, create a dedicated session manager:
import 'dart:async';
import 'package:dynamic_sdk/dynamic_sdk.dart';
import 'package:flutter/foundation.dart';
class SessionManager extends ChangeNotifier {
SessionManager() {
_initialize();
}
bool _isReady = false;
bool _isAuthenticated = false;
bool _isLoading = true;
dynamic _user;
List<BaseWallet> _wallets = [];
StreamSubscription<bool>? _readySub;
StreamSubscription<String?>? _tokenSub;
StreamSubscription<dynamic>? _userSub;
StreamSubscription<List<BaseWallet>>? _walletsSub;
bool get isReady => _isReady;
bool get isAuthenticated => _isAuthenticated;
bool get isLoading => _isLoading;
dynamic get user => _user;
List<BaseWallet> get wallets => _wallets;
void _initialize() {
// Listen to SDK ready state
_readySub = DynamicSDK.instance.sdk.readyChanges.listen((ready) {
_isReady = ready;
_updateLoadingState();
notifyListeners();
});
// Listen to auth token changes
_tokenSub = DynamicSDK.instance.auth.tokenChanges.listen((token) {
_isAuthenticated = token != null;
_updateLoadingState();
notifyListeners();
});
// Listen to user changes
_userSub = DynamicSDK.instance.auth.authenticatedUserChanges.listen((user) {
_user = user;
notifyListeners();
});
// Listen to wallet changes
_walletsSub = DynamicSDK.instance.wallets.userWalletsChanges.listen((wallets) {
_wallets = wallets;
notifyListeners();
});
}
void _updateLoadingState() {
_isLoading = !_isReady;
}
Future<void> logout() async {
try {
await DynamicSDK.instance.auth.logout();
} catch (e) {
debugPrint('Logout error: $e');
rethrow;
}
}
@override
void dispose() {
_readySub?.cancel();
_tokenSub?.cancel();
_userSub?.cancel();
_walletsSub?.cancel();
super.dispose();
}
}
3. Using Session Manager with Provider
import 'package:dynamic_sdk/dynamic_sdk.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
DynamicSDK.init(
props: ClientProps(
environmentId: 'your-environment-id',
appLogoUrl: 'https://your-app.com/logo.png',
appName: 'Your App',
),
);
runApp(
ChangeNotifierProvider(
create: (_) => SessionManager(),
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Dynamic App',
home: Stack(
children: [
const AppContent(),
DynamicSDK.instance.dynamicWidget,
],
),
);
}
}
class AppContent extends StatelessWidget {
const AppContent({super.key});
@override
Widget build(BuildContext context) {
return Consumer<SessionManager>(
builder: (context, session, child) {
if (session.isLoading) {
return const LoadingView();
}
if (session.isAuthenticated) {
return MainAppView(
user: session.user,
wallets: session.wallets,
);
}
return const LoginView();
},
);
}
}
class MainAppView extends StatelessWidget {
final dynamic user;
final List<BaseWallet> wallets;
const MainAppView({
super.key,
required this.user,
required this.wallets,
});
@override
Widget build(BuildContext context) {
final session = context.read<SessionManager>();
return Scaffold(
appBar: AppBar(
title: const Text('My App'),
actions: [
IconButton(
icon: const Icon(Icons.account_circle),
onPressed: () {
DynamicSDK.instance.ui.showUserProfile();
},
),
IconButton(
icon: const Icon(Icons.logout),
onPressed: () async {
await session.logout();
},
),
],
),
body: Column(
children: [
if (user != null)
Padding(
padding: const EdgeInsets.all(16.0),
child: Text('Welcome, ${user.email ?? "User"}!'),
),
if (wallets.isNotEmpty)
Expanded(
child: ListView.builder(
itemCount: wallets.length,
itemBuilder: (context, index) {
final wallet = wallets[index];
return ListTile(
title: Text(wallet.address),
subtitle: Text('Chain: ${wallet.chain}'),
);
},
),
),
],
),
);
}
}
Listening to Specific State Changes
Authentication Token Changes
StreamBuilder<String?>(
stream: DynamicSDK.instance.auth.tokenChanges,
builder: (context, snapshot) {
final token = snapshot.data;
if (token != null) {
// User is authenticated
return AuthenticatedContent();
} else {
// User is not authenticated
return UnauthenticatedContent();
}
},
)
User Profile Changes
StreamBuilder<dynamic>(
stream: DynamicSDK.instance.auth.authenticatedUserChanges,
builder: (context, snapshot) {
final user = snapshot.data;
if (user != null) {
return Column(
children: [
Text('Email: ${user.email}'),
Text('User ID: ${user.userId}'),
],
);
}
return const Text('No user logged in');
},
)
Wallet Changes
StreamBuilder<List<BaseWallet>>(
stream: DynamicSDK.instance.wallets.userWalletsChanges,
builder: (context, snapshot) {
final wallets = snapshot.data ?? [];
if (wallets.isEmpty) {
return const Text('No wallets available');
}
return ListView.builder(
itemCount: wallets.length,
itemBuilder: (context, index) {
final wallet = wallets[index];
return WalletCard(wallet: wallet);
},
);
},
)
SDK Ready State
StreamBuilder<bool>(
stream: DynamicSDK.instance.sdk.readyChanges,
builder: (context, snapshot) {
final isReady = snapshot.data ?? false;
if (!isReady) {
return const Center(
child: CircularProgressIndicator(),
);
}
return const AppContent();
},
)
Session Persistence
The Dynamic SDK automatically persists sessions across app restarts. When your app launches:
- Initialize the SDK in
main()
- Wait for
readyChanges to emit true
- Check
tokenChanges or authenticatedUserChanges for existing session
- If a valid session exists, the user is automatically authenticated
void main() {
WidgetsFlutterBinding.ensureInitialized();
// Initialize SDK - session restored automatically if valid
DynamicSDK.init(
props: ClientProps(
environmentId: 'your-environment-id',
appLogoUrl: 'https://your-app.com/logo.png',
appName: 'Your App',
),
);
runApp(const MyApp());
}
Manual Session Check
You can manually check the current session state:
// Check if user is authenticated
final token = DynamicSDK.instance.auth.token;
final isAuthenticated = token != null;
// Get current user
final user = DynamicSDK.instance.auth.authenticatedUser;
// Get current wallets
final wallets = DynamicSDK.instance.wallets.userWallets;
if (isAuthenticated && user != null) {
print('User ${user.userId} is authenticated');
print('Wallets: ${wallets.length}');
}
Logout
To end a user’s session:
Future<void> handleLogout() async {
try {
await DynamicSDK.instance.auth.logout();
// Session cleared - streams will emit updates
// UI will automatically update via StreamBuilders
} catch (e) {
// Handle error
print('Logout error: $e');
}
}
Best Practices
1. Always Use StreamBuilders
Use StreamBuilder widgets to automatically update UI when session state changes:
// Good ✓
StreamBuilder<String?>(
stream: DynamicSDK.instance.auth.tokenChanges,
builder: (context, snapshot) {
return snapshot.data != null ? HomeView() : LoginView();
},
)
// Bad ✗ - Won't update automatically
final token = DynamicSDK.instance.auth.token;
return token != null ? HomeView() : LoginView();
Always include DynamicSDK.instance.dynamicWidget in your widget tree:
Stack(
children: [
YourAppContent(),
DynamicSDK.instance.dynamicWidget, // Required for auth UI
],
)
3. Wait for SDK Ready
Always check that the SDK is ready before using it:
StreamBuilder<bool>(
stream: DynamicSDK.instance.sdk.readyChanges,
builder: (context, snapshot) {
if (snapshot.data != true) {
return LoadingView();
}
return AppContent();
},
)
4. Dispose Subscriptions
If manually subscribing to streams, always cancel subscriptions:
class _MyWidgetState extends State<MyWidget> {
late StreamSubscription _subscription;
@override
void initState() {
super.initState();
_subscription = DynamicSDK.instance.auth.tokenChanges.listen((token) {
// Handle token changes
});
}
@override
void dispose() {
_subscription.cancel();
super.dispose();
}
}
Troubleshooting
Session Not Persisting
- Ensure SDK is initialized in
main() before runApp()
- Check that
DynamicSDK.instance.dynamicWidget is included in widget tree
- Verify you’re not clearing app data or cache between sessions
UI Not Updating
- Make sure you’re using
StreamBuilder to listen to state changes
- Check that streams are being subscribed to correctly
- Verify the widget tree is being rebuilt when state changes
Token Expired
The SDK automatically handles token refresh. If you see authentication failures:
- Check network connectivity
- Verify your environment ID is correct
- Check dashboard settings for session duration
What’s Next
Now that you understand session management:
- Wallet Creation - Learn about automatic wallet creation after authentication
- Go Router Integration - Integrate session management with go_router navigation
- Token Balances - Display and manage wallet balances
- SDK Reference - Explore the complete SDK API
Reference
For more details on session management APIs, see: