Core Concepts

Reactivity

authStateChanges stream, sessionStream, synchronous state cache, and integration with StreamBuilder, Riverpod, Bloc, and GoRouter.

AuthyraInstance exposes two broadcast streams and a synchronous state cache. Every auth event — sign-in, sign-out, token refresh, session restore — propagates through these streams automatically. Wire them to any state management solution or UI primitive.


Streams

authStateChanges

Stream<AuthState> get authStateChanges

The primary stream. Emits every time the auth state transitions:

EventEmitted state
initialize() restores a sessionAuthState.authenticated(user)
initialize() finds no sessionAuthState.unauthenticated()
signIn() succeedsAuthState.authenticated(user)
signOut() completesAuthState.unauthenticated()
Token refresh succeedsAuthState.authenticated(user) (same user, new token)
Token refresh fails (expired)AuthState.unauthenticated()
Auth errorAuthState.error(message)

Because AuthState uses Equatable, identical consecutive states are deduplicated — no spurious widget rebuilds.

sessionStream

Stream<AuthSession?> get sessionStream

The lower-level stream carrying the full AuthSession — includes tokens, expiresAt, providerId, and linkedProviders. Use this when you need token data reactively (e.g., to update an HTTP interceptor).

Authyra.instance.sessionStream.listen((session) {
  if (session != null) {
    httpClient.token = session.accessToken;
  }
});

Synchronous state cache

AuthyraInstance maintains an in-memory cache updated synchronously on every state change. Safe to call in build() without setState or await:

// All synchronous — no await, no FutureBuilder
final AuthUser?  user   = Authyra.instance.currentUser;
final bool       auth   = Authyra.instance.isAuthenticated;
final AuthState  state  = Authyra.instance.currentState;

Use these for fast reads in build() or router redirect callbacks. For token access (which may trigger a silent refresh), use the async accessors:

final String?      token   = await Authyra.instance.getAccessToken();
final AuthSession? session = await Authyra.instance.getSession();

StreamBuilder

The simplest Flutter integration — no packages required:

StreamBuilder<AuthState>(
  stream: Authyra.instance.authStateChanges,
  builder: (context, snapshot) {
    final state = snapshot.data ?? AuthState.unauthenticated();
    return switch (state.type) {
      AuthStateType.authenticated   => HomePage(user: state.user!),
      AuthStateType.unauthenticated => const LoginPage(),
      AuthStateType.error           => ErrorPage(state.error!),
    };
  },
)

The ?? AuthState.unauthenticated() fallback handles the brief moment before initialize() emits the first value.


Riverpod

// Define a StreamProvider
final authStateProvider = StreamProvider<AuthState>((ref) {
  return Authyra.instance.authStateChanges;
});

// In a widget
final state = ref.watch(authStateProvider).value ?? AuthState.unauthenticated();

return switch (state.type) {
  AuthStateType.authenticated   => HomePage(user: state.user!),
  AuthStateType.unauthenticated => const LoginPage(),
  AuthStateType.error           => ErrorPage(state.error!),
};

For token-aware data fetching, combine with sessionStream:

final sessionProvider = StreamProvider<AuthSession?>((ref) {
  return Authyra.instance.sessionStream;
});

Bloc / Cubit

class AuthCubit extends Cubit<AuthState> {
  late final StreamSubscription<AuthState> _sub;

  AuthCubit() : super(AuthState.unauthenticated()) {
    _sub = Authyra.instance.authStateChanges.listen(emit);
  }

  Future<void> signIn(String email, String password) async {
    try {
      await Authyra.instance.signIn('email', params: {
        'email': email, 'password': password,
      });
    } on AuthenticationFailedException {
      emit(AuthState.error('Invalid credentials'));
    }
  }

  Future<void> signOut() => Authyra.instance.signOut();

  @override
  Future<void> close() {
    _sub.cancel();
    return super.close();
  }
}

GoRouter

Wire authStateChanges to refreshListenable for automatic route redirects:

lib/router.dart
import 'package:authyra_flutter/authyra_flutter.dart';
import 'package:go_router/go_router.dart';

// Minimal Listenable adapter for go_router
class _StreamListenable extends ChangeNotifier {
  _StreamListenable(Stream<dynamic> stream) {
    stream.listen((_) => notifyListeners());
  }
}

final router = GoRouter(
  refreshListenable: _StreamListenable(Authyra.instance.authStateChanges),
  redirect: (context, state) {
    final authenticated = Authyra.instance.isAuthenticated;  // synchronous
    final goingToLogin  = state.uri.path.startsWith('/login');

    if (!authenticated && !goingToLogin) return '/login';
    if (authenticated  &&  goingToLogin) return '/';
    return null;
  },
  routes: [
    GoRoute(path: '/login', builder: (_, __) => const LoginPage()),
    GoRoute(path: '/',      builder: (_, __) => const HomePage()),
  ],
);

isAuthenticated is synchronous — safe to call inside redirect without await.


Direct stream listener

Subscribe directly when you need side effects on state changes:

late final StreamSubscription<AuthState> _sub;

@override
void initState() {
  super.initState();
  _sub = Authyra.instance.authStateChanges.listen((state) {
    if (state.isAuthenticated) {
      Analytics.identify(state.user!.id);
    } else {
      Analytics.reset();
    }
  });
}

@override
void dispose() {
  _sub.cancel();
  super.dispose();
}

Initial state before initialization

authStateChanges is a broadcast stream — it does not replay past events. If you subscribe after Authyra.initialize() has completed, the stream won't replay the initial state.

Use currentState for an immediate synchronous read, or use StreamBuilder which handles the connectionState == waiting case via the fallback value:

// Safe pattern: fallback to unauthenticated until first emission
final state = snapshot.data ?? AuthState.unauthenticated();

See also

Copyright © 2026