The Dynamic Flutter SDK integrates seamlessly with go_router for navigation and route protection based on authentication state. This guide demonstrates how to set up go_router with Dynamic authentication flows.

Overview

The integration involves creating an AuthNotifier that bridges Dynamic SDK streams into a ChangeNotifier that go_router can use to automatically handle route redirects based on authentication state.

Complete Implementation

1. AuthNotifier Setup

First, create an AuthNotifier class that listens to Dynamic SDK authentication streams:
import 'dart:async';
import 'package:dynamic_sdk/dynamic_sdk.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

/// Simple enum to reason about routing
enum AuthPhase { loading, unauthenticated, authenticated }

/// Bridges DynamicSDK streams into a ChangeNotifier for go_router
class AuthNotifier extends ChangeNotifier {
  AuthPhase _phase = AuthPhase.loading;
  AuthPhase get phase => _phase;

  StreamSubscription<bool>? _readySub;
  StreamSubscription<dynamic /*User?*/>? _userSub;

  bool _ready = false;
  bool _hasUser = false;

  AuthNotifier() {
    // Listen to SDK readiness
    _readySub = DynamicSDK.instance.sdk.readyChanges.listen((ready) {
      _ready = ready;
      _recomputePhase();
    });

    // Listen to auth state changes
    _userSub = DynamicSDK.instance.auth.authenticatedUserChanges.listen((user) {
      _hasUser = user != null;
      _recomputePhase();
    });
  }

  void _recomputePhase() {
    final next = !_ready
        ? AuthPhase.loading
        : (_hasUser ? AuthPhase.authenticated : AuthPhase.unauthenticated);

    if (next != _phase) {
      _phase = next;
      notifyListeners();
    }
  }

  @override
  void dispose() {
    _readySub?.cancel();
    _userSub?.cancel();
    super.dispose();
  }
}

2. Go Router Configuration

Set up your go_router with the AuthNotifier and route protection:
class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  late final AuthNotifier _auth;
  late final GoRouter _router;

  @override
  void initState() {
    super.initState();
    _auth = AuthNotifier();

    _router = GoRouter(
      // This makes go_router re-check redirect whenever auth/ready changes
      refreshListenable: _auth,
      routes: [
        GoRoute(path: '/', builder: (_, __) => const HomeView()),
        GoRoute(path: '/login', builder: (_, __) => const LoginView()),
        GoRoute(path: '/loading', builder: (_, __) => const LoadingView()),
        GoRoute(
          path: '/wallet/:walletId',
          builder: (_, state) =>
              WalletView(walletId: state.pathParameters['walletId']!),
        ),
      ],
      redirect: (context, state) {
        final inLogin = state.matchedLocation == '/login';
        final inLoading = state.matchedLocation == '/loading';

        switch (_auth.phase) {
          case AuthPhase.loading:
            // While SDK/user are loading, keep users on /loading
            return inLoading ? null : '/loading';

          case AuthPhase.unauthenticated:
            // If not logged in, force /login except when already there
            return inLogin ? null : '/login';

          case AuthPhase.authenticated:
            // If logged in, keep them off /login and /loading
            if (inLogin || inLoading) return '/';
            return null;
        }
      },
    );
  }

  @override
  void dispose() {
    _auth.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Flutter Demo',
      routerDelegate: _router.routerDelegate,
      routeInformationParser: _router.routeInformationParser,
      routeInformationProvider: _router.routeInformationProvider,
      builder: (context, child) {
        if (child == null) return const SizedBox.shrink();

        return Stack(
          children: [
            child,
            // Keep Dynamic overlay/widget above your pages
            DynamicSDK.instance.dynamicWidget,
          ],
        );
      },
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
    );
  }
}

3. Dynamic SDK Initialization

Initialize the Dynamic SDK in your main() function:
void main() {
  WidgetsFlutterBinding.ensureInitialized();
  DynamicSDK.init(
    props: ClientProps(
      environmentId: 'your-environment-id',
      appLogoUrl: 'https://your-app.com/logo.png',
      appName: 'Your App Name',
      redirectUrl: "yourapp://",
    ),
  );
  runApp(const MyApp());
}

Route Protection Patterns

Authentication States

The AuthNotifier manages three distinct states:
  • AuthPhase.loading: SDK is initializing or user state is being determined
  • AuthPhase.unauthenticated: SDK is ready but no user is authenticated
  • AuthPhase.authenticated: SDK is ready and user is authenticated

Redirect Logic

The redirect function automatically handles navigation based on authentication state:
redirect: (context, state) {
  final inLogin = state.matchedLocation == '/login';
  final inLoading = state.matchedLocation == '/loading';

  switch (_auth.phase) {
    case AuthPhase.loading:
      return inLoading ? null : '/loading';

    case AuthPhase.unauthenticated:
      return inLogin ? null : '/login';

    case AuthPhase.authenticated:
      if (inLogin || inLoading) return '/';
      return null;
  }
}
Navigate programmatically using go_router methods:
// Navigate to a specific wallet
context.push('/wallet/${wallet.id}');

// Navigate to home after logout
context.go('/');

// Navigate with parameters
context.pushNamed('wallet', pathParameters: {'walletId': walletId});

Integration with Dynamic UI

Login View

class LoginView extends StatelessWidget {
  const LoginView({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Login')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            DynamicSDK.instance.ui.showAuth();
          },
          child: const Text('Login with Dynamic'),
        ),
      ),
    );
  }
}

Loading View

class LoadingView extends StatelessWidget {
  const LoadingView({super.key});

  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Center(
        child: CircularProgressIndicator(),
      ),
    );
  }
}

Live Example

For a complete working example, check out the Dynamic Flutter Example App which demonstrates this exact integration pattern with additional features like wallet management and web3dart integration. You can read more about go_router in the official documentation.