Skip to main content
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:
  1. Initialize the SDK in main()
  2. Wait for readyChanges to emit true
  3. Check tokenChanges or authenticatedUserChanges for existing session
  4. 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();

2. Include Dynamic Widget Overlay

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:
  1. Wallet Creation - Learn about automatic wallet creation after authentication
  2. Go Router Integration - Integrate session management with go_router navigation
  3. Token Balances - Display and manage wallet balances
  4. SDK Reference - Explore the complete SDK API

Reference

For more details on session management APIs, see: