Home Flutter How to Manage Authentication State Across Flutter Screens

How to Manage Authentication State Across Flutter Screens

7
0

Authentication in Flutter becomes difficult when individual screens maintain separate versions of a user’s session. A login screen may report success while the profile page shows outdated information. After logout, navigation history may expose a screen that should no longer be available.

A reliable approach uses one shared authentication state, observes session changes, and lets navigation respond consistently.

This guide explains that approach using Firebase Authentication and go_router, with practical considerations for session restoration, logout, and protected screens.

What Is Authentication State?

Authentication state represents what the application currently knows about a user’s session.

StateMeaningScreen behavior
InitializingThe session is being restoredShow a loading screen
Signed outNo authenticated user is availableAllow public screens
Signed inAn authenticated user is availableAllow permitted screens
ErrorSession initialization or observation failedShow recovery options

A single isLoggedIn boolean cannot distinguish initialization from an unauthenticated session. That distinction prevents the login screen from briefly appearing while an existing session loads.

Authentication also differs from authorization. Signing in establishes identity; access to a specific resource still depends on permissions.

Keep Authentication State Above Individual Screens

A maintainable Flutter authentication architecture separates responsibilities:

  1. Authentication repository: Communicates with the identity provider.
  2. Session controller: Exposes observable authentication state.
  3. Router: Applies navigation rules.
  4. Screens: Display information and collect user input.

This aligns with Flutter’s guidance on separating UI and data responsibilities. Flutter architecture guide

Local form state should remain local. Password visibility, field validation, and button-loading indicators do not need to become application-wide session state.

Choose the Correct Firebase Authentication Stream

Firebase provides three streams with different purposes:

StreamWhat it observes
authStateChanges()Initial authentication state, sign-in, and sign-out
idTokenChanges()Authentication events and ID-token changes
userChanges()Token events and supported user-profile operations

For basic protected navigation, authStateChanges() is usually sufficient. Applications displaying token-dependent information or profile updates may need the other streams.

Administrative changes, including remotely disabling an account, do not automatically trigger corresponding client events in every situation. Firebase authentication documentation

Create a Shared Session Controller

The following controller observes Firebase and notifies its consumers when the session changes.

Firebase must already be configured and initialized.

import 'dart:async';

import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/foundation.dart';

enum SessionStatus {
  initializing,
  signedOut,
  signedIn,
  error,
}

class SessionController extends ChangeNotifier {
  SessionController(this._auth) {
    _subscription = _auth.authStateChanges().listen(
      (user) {
        _user = user;
        _status = user == null
            ? SessionStatus.signedOut
            : SessionStatus.signedIn;
        notifyListeners();
      },
      onError: (Object error, StackTrace stackTrace) {
        _user = null;
        _status = SessionStatus.error;
        notifyListeners();
      },
    );
  }

  final FirebaseAuth _auth;
  late final StreamSubscription<User?> _subscription;

  SessionStatus _status = SessionStatus.initializing;
  User? _user;

  SessionStatus get status => _status;
  User? get user => _user;

  Future<void> signIn(String email, String password) async {
    await _auth.signInWithEmailAndPassword(
      email: email,
      password: password,
    );
  }

  Future<void> signOut() => _auth.signOut();

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

The controller does not manually mark login as successful. Firebase’s session event updates the state.

Failed sign-in attempts should produce form-level errors. They should not be confused with failures to initialize or observe the session.

ChangeNotifier supplies the notification mechanism, and its owner must dispose of it appropriately. Flutter ChangeNotifier documentation

In larger applications, placing a repository between Firebase and the controller makes authentication easier to replace and test.

Protect Screens Through Centralized Routing

Navigation rules should react to the shared session rather than being repeated inside every screen.

go_router supports redirection and a refreshListenable that can trigger route reevaluation when the controller changes. GoRouter API

The following fragment belongs inside a GoRouter configuration:

refreshListenable: session,
redirect: (context, state) {
  final path = state.uri.path;

  switch (session.status) {
    case SessionStatus.initializing:
      return path == '/loading' ? null : '/loading';

    case SessionStatus.error:
      return path == '/session-error'
          ? null
          : '/session-error';

    case SessionStatus.signedOut:
      final isPublic =
          path == '/login' || path == '/privacy';
      return isPublic ? null : '/login';

    case SessionStatus.signedIn:
      final isSessionPage =
          path == '/login' ||
          path == '/loading' ||
          path == '/session-error';

      return isSessionPage ? '/' : null;
  }
},

The application must define the referenced routes. The error screen should offer a way to retry session initialization.

Returning null permits navigation; returning a location requests a redirect. Avoid redirecting repeatedly to the same destination. go_router redirection guide

Create the controller and router once in an application-level owner. Recreating them inside build() can reset routing state or create unnecessary subscriptions.

Preserve protected deep links

This simplified configuration returns users to / after login or initialization.

For a destination such as /orders/123, preserve the intended internal URI before redirecting. Restore it after authentication, provided it remains valid and permitted.

Return destinations should be restricted to approved application routes.

Share the Same Session Across Screens

Screens can observe the controller through constructor injection or the application’s existing dependency-injection system.

ListenableBuilder(
  listenable: session,
  builder: (context, child) {
    return Text(
      session.user?.email ?? 'No active session',
    );
  },
)

The important detail is that every consumer receives the same controller instance.

Applications using Riverpod or Bloc can implement this pattern through their existing state-management approach. Introducing another library is unnecessary when the current setup already supports shared, observable state.

Profile information may require separate observation. A display-name change does not necessarily represent a sign-in or sign-out event. Firebase documents profile updates and user reload behavior separately. Firebase user management

Restore Sessions Without Duplicating Login Flags

Firebase supports session persistence across application restarts, with configurable persistence behavior on the web. A separately stored isLoggedIn flag can become inconsistent with the actual session. Firebase persistence documentation

The initializing state gives the authentication layer time to restore the session before navigation decisions occur.

With a custom backend, restoration should include the appropriate credential validation or refresh process. Finding a stored token alone should not establish that the session remains valid.

Make Logout Clear User-Specific State

Logout affects more than the visible screen. Applications should also invalidate or dispose of:

  • User-specific subscriptions.
  • Cached account and profile data.
  • Selected organizations or workspaces.
  • Pending requests associated with the previous session.

For example, a request started by user A could finish after user B signs in. Its result must not update user B’s interface.

User-scoped repositories or session identifiers help prevent this cross-session leakage.

Protected navigation should also remain enforced when a user presses Back, opens browser history, or follows a deep link.

Enforce Authorization on the Backend

Flutter route guards control presentation. They do not secure an API or database.

Backend services must validate credentials and enforce permissions independently. Local sign-out also differs from revoking sessions across devices. Firebase documents token lifetimes and server-side revocation handling in its session-management guide. Firebase session management

An access-denied response should not automatically log out a valid user. Authentication failure and insufficient permission require different handling.

Test Authentication Transitions

A useful test plan covers more than successful login:

ScenarioExpected result
Startup without a sessionLoading, then login
Startup with a restored sessionLoading, then permitted content
Incorrect credentialsForm error without authenticated navigation
Logout from a nested screenProtected content becomes inaccessible
Protected deep linkAuthentication followed by a validated destination
Session observation failureRecovery screen
User switch during a requestPrevious session’s response is discarded

Controller tests can use a fake authentication repository. Widget and routing tests should verify that navigation matches session state.

Frequently Asked Questions

Is setState() enough for authentication?

It works for local form behavior. Authentication shared across screens needs a common owner so that session information and navigation stay synchronized.

Can StreamBuilder manage authentication?

Yes, especially for a simple root authentication gate. Flutter recommends obtaining the stream before build() rather than creating it during every rebuild. StreamBuilder documentation

The application must still handle navigation stacks correctly. Replacing a home widget alone may leave separately pushed routes visible.

Should the login button navigate after sign-in?

When the router observes authentication state, the session change can drive navigation. The button can focus on submitting credentials and displaying errors.

What is the key architectural principle?

Every screen should derive authentication information from one observable source. This keeps sign-in, restoration, protected navigation, and logout consistent.

Previous articleHow to Manage Authentication State Across Flutter Screens

LEAVE A REPLY

Please enter your comment!
Please enter your name here