Reactivity
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:
| Event | Emitted state |
|---|---|
initialize() restores a session | AuthState.authenticated(user) |
initialize() finds no session | AuthState.unauthenticated() |
signIn() succeeds | AuthState.authenticated(user) |
signOut() completes | AuthState.unauthenticated() |
| Token refresh succeeds | AuthState.authenticated(user) (same user, new token) |
| Token refresh fails (expired) | AuthState.unauthenticated() |
| Auth error | AuthState.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:
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
- Sessions → —
AuthSessionfields and token access - Architecture → —
AuthyraClientvsAuthyraInstance - Route Protection guide →
- Flutter Setup →