In firebase_auth, how do authStateChanges, idTokenChanges and userChanges differ, and which one should drive sign-in versus home routing?
answer
- three streams, one nullable User
- sign-in and sign-out only
- plus token refresh
- superset: profile updates and reload
- route on the narrowest one
basics
~20 sAll 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 linesimport '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
Name the three streams and recall that each emits a User? that is null when signed out, starting right after you subscribe.
Lay out which events wake each stream — sign-in and sign-out, token refresh, profile updates — and justify authStateChanges for the routing gate.
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.
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?.