skip to content

In firebase_auth, how do authStateChanges, idTokenChanges and userChanges differ, and which one should drive sign-in versus home routing?

level: middleimportance: must knowfreq 62%

answer

  1. three streams, one nullable User
  2. sign-in and sign-out only
  3. plus token refresh
  4. superset: profile updates and reload
  5. route on the narrowest one

basics

~20 s

All three emit a User? on subscribe, sign-in and sign-out; idTokenChanges also fires on token refresh, and userChanges also on profile updates, linking and reload. Route between sign-in and home on authStateChanges, the only one limited to that decision.

solid answer

~40 s

`FirebaseAuth.instance` offers three `Stream<User?>` methods. `authStateChanges()` fires right after you subscribe and on every sign-in and sign-out. `idTokenChanges()` adds the current user's ID token changing, such as a refresh. `userChanges()` is the superset: it also fires on `reload()`, `unlink()` and profile updates such as `updateProfile()` or `updatePassword()`. For a sign-in versus home gate, `authStateChanges()` is the right input, because it changes exactly when the answer can change; routing on `userChanges()` re-runs the routing on every profile edit and token refresh. None of them fires for changes made with the Admin SDK or for a user disabled in the console — the client only learns of those after `currentUser!.reload()`.

code

dart · 17 lines
dart
import 'package:firebase_auth/firebase_auth.dart';

class AuthService {
  AuthService(this._auth)
      : signedIn = _auth
            .authStateChanges()
            .map((User? user) => user != null)
            .distinct();

  final FirebaseAuth _auth;

  /// Drives the sign-in vs recipe-feed gate.
  final Stream<bool> signedIn;

  /// Drives the account page that shows the cook's name and photo.
  Stream<User?> get profile => _auth.userChanges();
}

go deeper

for a junior

Name the three streams and recall that each emits a User? that is null when signed out, starting right after you subscribe.

for a middle

Lay out which events wake each stream — sign-in and sign-out, token refresh, profile updates — and justify authStateChanges for the routing gate.

for a senior

Handle what the streams miss: Admin SDK edits, disabled users and new claims. Keep one stream instance and narrow it before it reaches the router.

for a principal

Define which layer owns auth state for the whole app, so routing, API clients and profile screens each subscribe to the narrowest signal they need.

## Three streams, one `User?` `firebase_auth` exposes the signed-in user as a nullable `User`. Besides the `currentUser` getter — a one-off snapshot — `FirebaseAuth.instance` offers three methods that each return a `Stream<User?>`. All three emit **immediately after a listener subscribes** (once the auth state has been initialized) and then again on later changes. They differ only in *which* changes wake them up. | Event | `authStateChanges()` | `idTokenChanges()` | `userChanges()` | |---|---|---|---| | Right after a listener registers | yes | yes | yes | | A user signs in | yes | yes | yes | | The current user signs out | yes | yes | yes | | The current user's ID token changes (refresh) | no | yes | yes | | `reload()`, `unlink()`, `updatePassword()`, `updatePhoneNumber()`, `updateProfile()` on the current user | no | no | yes | The `userChanges()` doc comment in the pinned source calls it "a superset of both `authStateChanges` and `idTokenChanges`": it exists so an app can follow profile edits — a new display name, a newly linked or unlinked provider — without calling `reload()` and re-reading the user by hand. (The FlutterFire guide still lists `updateEmail()` in this set; that method is gone from the current `User` class, which offers `verifyBeforeUpdateEmail()` instead.) ## Which one should drive routing In a recipe-sharing app the top-level question is binary: is anyone signed in? If yes, show the recipe feed; if no, show the sign-in screen. That is exactly the question `authStateChanges()` answers, and it answers *only* that question. - **`authStateChanges()`** — the default for routing. It fires on sign-in and sign-out, so a route decision is re-evaluated only when the answer can actually change. - **`idTokenChanges()`** — for code that must react when the token itself changes: re-attaching a fresh token to an API client, or noticing new custom claims after a refresh. - **`userChanges()`** — for screens that display profile data, such as the account page showing the cook's display name and photo. Routing on `userChanges()` is not *wrong* — the null/non-null answer is the same — but every profile edit and every token refresh now re-runs the routing logic. In a router that rebuilds its page stack on each event, that means needless rebuilds and, in the worst case, a user dropped back to the home route while editing their profile. ## What none of them report The FlutterFire guide is explicit about the gaps: - Changes made **server-side with the Admin SDK** (a profile update) do not fire any of the three streams. The client sees them after `currentUser!.reload()`. - A user **disabled or deleted in the console** keeps appearing signed in on the device until something forces a round trip; `reload()` then throws a `FirebaseAuthException` with `user-disabled` or `user-not-found`, which the app should catch and treat as a sign-out. - Custom-claim changes reach `idTokenChanges()` only when a new ID token is issued — at the next sign-in, the next natural refresh, or a forced refresh with `getIdTokenResult(true)`. ## Matching each consumer to a signal A useful habit in an interview answer is to name the consumer first and pick the signal second: 1. **The sign-in versus recipe-feed gate** subscribes to `authStateChanges()`. 2. **An API client or socket that holds a token** subscribes to `idTokenChanges()`, so it can re-authenticate when the token rotates. 3. **The account screen** showing name, photo and linked providers subscribes to `userChanges()`. 4. **A one-off read after startup**, such as taking the `uid` to build a query, uses the `currentUser` snapshot. Each consumer then wakes up only for events that change its own output. ## Mechanics worth knowing 1. Each call to one of these methods returns a **new** broadcast `Stream` object in the pinned `firebase_auth` source. Create the stream once — in a field, a provider or a service — rather than calling `authStateChanges()` on every rebuild, or each rebuild resubscribes. 2. The events carry `User?`, not `UserCredential`; the credential only comes back from the sign-in call itself. 3. `null` means signed out. There is no separate "loading" event: until the first event arrives, your app is in the loading state and should render something neutral, such as a splash. ```dart import 'package:firebase_auth/firebase_auth.dart'; enum Gate { signedOut, signedIn } // Created once, e.g. in a service, and shared by the router. final Stream<Gate> gateStream = FirebaseAuth.instance .authStateChanges() .map((User? user) => user == null ? Gate.signedOut : Gate.signedIn) .distinct(); ``` Mapping to a two-value enum with `distinct()` makes the routing input as narrow as the decision it drives, whichever stream sits underneath.

  • A user is disabled in the Firebase console. Why does the app still show them as signed in?
    None of the three streams fires for a user disabled or deleted through the console or the Admin SDK. The device keeps its session until something forces a round trip: calling `currentUser!.reload()` then throws a `FirebaseAuthException` with `user-disabled` or `user-not-found`, which the app should treat as a sign-out.
  • Why store the stream in a field instead of calling authStateChanges() inside build?
    In the pinned firebase_auth source, each call to `authStateChanges()` builds a new broadcast `Stream` object. Calling it on every build hands the widget layer a different stream each time, so it unsubscribes and resubscribes on each rebuild. Creating it once keeps a single subscription.
  • When does idTokenChanges() pick up a custom claim that a backend just set?
    Only when a new ID token is issued: at the next sign-in or re-authentication, at the next natural refresh after the old token expires, or when the client forces one with `getIdTokenResult(true)`. Setting the claim on the server does not push anything to the device by itself.

authStateChanges is the doorbell that rings only when someone enters or leaves; idTokenChanges also rings when a guest's wristband is renewed; userChanges rings whenever a guest changes their name tag. Route on the doorbell.

saying these in an interview costs you the question

  • authStateChanges() fires whenever the ID token refreshes.
  • userChanges() is the safest stream for routing because it reports everything.
  • The streams only emit on later changes, never on subscribe.
  • Updating a user with the Admin SDK immediately fires userChanges() on the device.
  • The streams emit a UserCredential rather than a User?.