Google Firebase in Flutter Login

Google Sign-In with Firebase in Flutter: Complete Tutorial (2026) | D.Tech Academic

Google Sign-In with Firebase in Flutter: Complete Tutorial (2026)

Last Updated: | Author: Deep Singh

Letting users sign in with their Google account is one of the quickest wins you can add to a Flutter app. No password to remember, no form to fill out, no "forgot password" flow to build. Firebase Authentication handles all the heavy lifting, and the google_sign_in plugin gives you the Google account picker with just a few lines of code.

In this tutorial, you'll build a complete Google Sign-In flow: a login screen with a "Sign in with Google" button, a home screen showing the signed-in user's email, and a working logout. We'll also cover the Firebase project setup and the Gradle changes you need.

Important: The plugin versions in the original 2021 tutorial are now several major versions out of date. firebase_auth: ^0.18.4+1, google_sign_in: ^4.5.3, and firebase_core: ^0.5.0 will not compile against current Flutter versions. This updated tutorial uses the current APIs.

Firebase Project Setup

Before writing any code, you need to connect your Flutter app to a Firebase project.

  1. Go to the Firebase Console and create a new project (or use an existing one).
  2. Add an Android app to the project, using your app's package name (found in android/app/build.gradle under applicationId).
  3. Download the generated google-services.json file and place it in android/app/.
  4. In the Firebase Console, go to Authentication → Sign-in method and enable Google.
  5. When enabling Google Sign-In, you'll be asked to provide a support email. Set it, save the change, and you're done with the console.

The recommended modern approach uses the FlutterFire CLI, which handles most of this automatically:

dart pub global activate flutterfire_cli
flutterfire configure

This generates a firebase_options.dart file and wires it into your app, avoiding manual google-services.json placement entirely.

Adding the Plugins

Add the plugins with a single command:

flutter pub add firebase_core firebase_auth google_sign_in

Or add them manually to your pubspec.yaml:

dependencies:
  firebase_core: ^3.6.0
  firebase_auth: ^5.3.1
  google_sign_in: ^6.2.1

Then run flutter pub get.

Version comparison:

  • Original: firebase_auth: ^0.18.4+1 → Current: firebase_auth: ^5.3.1 (5 major versions newer)
  • Original: google_sign_in: ^4.5.3 → Current: google_sign_in: ^6.2.1
  • Original: firebase_core: ^0.5.0 → Current: firebase_core: ^3.6.0

The APIs changed significantly. Most notably, Firebase.initializeApp() must now be called in main() before any Firebase plugin is used.

Gradle Configuration

In the modern setup, you don't need most of the boilerplate from the 2021 tutorial. Here's what's actually required.

android/app/build.gradle

Add the Google Services plugin at the top of the file:

plugins {
    id "com.android.application"
    id "kotlin-android"
    id "dev.flutter.flutter-gradle-plugin"
    id "com.google.gms.google-services"   // ← add this line
}

Note: You do not need to manually add the Firebase BoM or the analytics dependency. The firebase_core plugin handles those via the Flutter tooling.

android/build.gradle

Under the buildscript → dependencies block, add the Google Services classpath:

buildscript {
    dependencies {
        classpath 'com.google.gms:google-services:4.4.2'
    }
}

In newer Flutter projects that use the plugins DSL in settings.gradle, you would instead declare:

plugins {
    id "com.google.gms.google-services" version "4.4.2" apply false
}

What was removed from the original: the original tutorial showed implementation platform('com.google.firebase:firebase-bom:26.1.1') and implementation 'com.google.firebase:firebase-analytics' inside the Android dependencies block. Those are no longer needed — the Flutter plugins pull in the right Firebase SDK versions automatically.

Also removed: jcenter() in the repositories blocks. JCenter was shut down in 2021. Use mavenCentral() and google() only.

Entry Point

Firebase must be initialised before the app runs. Update main.dart:

import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';

import 'firebase/google_auth.dart';
import 'firebase/firebase_options.dart';

Future<void> main() async {
    WidgetsFlutterBinding.ensureInitialized();
    await Firebase.initializeApp(
        options: DefaultFirebaseOptions.currentPlatform,
    );

    runApp(const MaterialApp(
        debugShowCheckedModeBanner: false,
        home: GoogleLoginDemo(),
    ));
}

Key change: The original called Firebase.initializeApp() inside initState() of the login widget. That works, but it's fragile — if any other part of the app touches Firebase before the login screen mounts, it crashes. The correct place is main(), before runApp().

The WidgetsFlutterBinding.ensureInitialized() call is required because we're awaiting an async operation before running the app.

If you're not using flutterfire configure, you can call Firebase.initializeApp() without the options parameter — Flutter will use the google-services.json (Android) or GoogleService-Info.plist (iOS) automatically.

Login Page

Here's the complete login widget with modern null safety and error handling.

import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:google_sign_in/google_sign_in.dart';

import 'homepage.dart';

class GoogleLoginDemo extends StatefulWidget {
    const GoogleLoginDemo({super.key});

    @override
    State<GoogleLoginDemo> createState() => _GoogleLoginDemoState();
}

class _GoogleLoginDemoState extends State<GoogleLoginDemo> {
    final FirebaseAuth _auth = FirebaseAuth.instance;
    final GoogleSignIn _googleSignIn = GoogleSignIn();
    bool _busy = false;

    @override
    void initState() {
        super.initState();
        _checkCurrentUser();
    }

    @override
    Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(title: const Text('Google Login')),
            body: Center(
                child: _busy
                    ? const CircularProgressIndicator()
                    : ElevatedButton.icon(
                        onPressed: _handleSignIn,
                        icon: const Icon(Icons.login),
                        label: const Text('Sign in with Google'),
                    ),
            ),
        );
    }

    /// If the user is already signed in (from a previous session),
    /// skip the login screen.
    void _checkCurrentUser() {
        final user = _auth.currentUser;
        if (user != null) {
            _goToHomePage(user);
        }
    }

    /// Handles the Google Sign-In flow.
    Future<void> _handleSignIn() async {
        setState(() => _busy = true);
        try {
            // 1. Check if already signed in to Google on the device
            final GoogleSignInAccount? googleUser =
                await _googleSignIn.signIn();

            if (googleUser == null) {
                // User dismissed the sign-in dialog
                return;
            }

            // 2. Get the auth details from the Google sign-in
            final GoogleSignInAuthentication googleAuth =
                await googleUser.authentication;

            // 3. Create a Firebase credential
            final credential = GoogleAuthProvider.credential(
                accessToken: googleAuth.accessToken,
                idToken: googleAuth.idToken,
            );

            // 4. Sign in to Firebase with the credential
            final userCredential =
                await _auth.signInWithCredential(credential);

            final user = userCredential.user;
            if (user != null) {
                _goToHomePage(user);
            }
        } catch (e) {
            if (mounted) {
                ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(content: Text('Sign-in failed: $e')),
                );
            }
        } finally {
            if (mounted) setState(() => _busy = false);
        }
    }

    void _goToHomePage(User user) {
        Navigator.pushReplacement(
            context,
            MaterialPageRoute(
                builder: (context) => FirebaseHomePage(user: user),
            ),
        );
    }
}

What's been fixed from the original:

  • Firebase initialisation moved out. No more initApplication() inside initState(). Firebase is already initialised in main().
  • Null safety throughout. All Firebase types are nullable (User?, GoogleSignInAccount?, etc.). The original code would fail null safety checks.
  • Handled the "user dismissed" case. If the user closes the Google account picker without selecting anything, signIn() returns null. The original code would crash on googleUser.authentication.
  • Error handling. Any failure during sign-in is caught and shown to the user. The original would silently fail or throw an unhandled exception.
  • Loading state. The button disables itself and shows a spinner while the sign-in is in progress, preventing double-taps.
  • Replaced Navigator.push with Navigator.pushReplacement. After login, the user shouldn't be able to go back to the login screen with the back button. Push-replace removes the login route from the stack.
  • Better button. ElevatedButton.icon with a login icon looks more like a real sign-in button than a plain text button.
  • Removed the unused _user assignment pattern. The original code had a subtle bug where _user was assigned but never used in the "already signed in to Google" branch.

Home Page

The home page shows the user's email and a logout button. It no longer needs to hold a reference to GoogleSignIn — the login flow handles that.

import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:google_sign_in/google_sign_in.dart';

class FirebaseHomePage extends StatelessWidget {
    final User user;

    const FirebaseHomePage({super.key, required this.user});

    Future<void> _signOut(BuildContext context) async {
        await GoogleSignIn().signOut();
        await FirebaseAuth.instance.signOut();

        if (context.mounted) {
            Navigator.of(context).popUntil((route) => route.isFirst);
        }
    }

    @override
    Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(title: const Text('Home Page')),
            body: Center(
                child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                        CircleAvatar(
                            radius: 40,
                            backgroundImage: user.photoURL != null
                                ? NetworkImage(user.photoURL!)
                                : null,
                            child: user.photoURL == null
                                ? Text(user.email?.substring(0, 1).toUpperCase() ?? '?')
                                : null,
                        ),
                        const SizedBox(height: 16),
                        Text(
                            user.displayName ?? 'No name',
                            style: const TextStyle(
                                fontSize: 20,
                                fontWeight: FontWeight.bold,
                            ),
                        ),
                        const SizedBox(height: 8),
                        Text(user.email ?? 'No email'),
                        const SizedBox(height: 24),
                        ElevatedButton.icon(
                            onPressed: () => _signOut(context),
                            icon: const Icon(Icons.logout),
                            label: const Text('Logout'),
                        ),
                    ],
                ),
            ),
        );
    }
}

What's been fixed from the original:

  • StatelessWidget instead of StatefulWidget. The original used a StatefulWidget but didn't actually manage any local state — a clear sign it should be stateless.
  • Both Google and Firebase sign-out. The original only called googleSignIn.signOut(). That clears the Google account selection but leaves the Firebase session active. On the next launch, the user would be auto-logged-in to Firebase. Now both are signed out.
  • Correct navigation after logout. Navigator.pop(context) only pops one route. If the user navigated deeper into the app, it wouldn't return to the login screen. popUntil((route) => route.isFirst) clears everything back to the login screen.
  • Null-safe display. user.displayName and user.photoURL can be null. The original called widget._user.email directly — would crash if email were null.
  • Better UI. Profile picture, display name, and email make for a much nicer home screen than the single email line in the original.
  • Removed the GoogleSignIn constructor parameter. It was passed in just to be called on logout. Instantiating GoogleSignIn() directly on the logout method is cleaner and works the same.

How the Code Works

Let's trace through the flow to see how all the pieces fit together.

1. App Startup

When main() runs, it calls Firebase.initializeApp() before mounting the app. From this point on, FirebaseAuth.instance is ready to use anywhere in the app.

2. Auto-Login Check

In initState(), _checkCurrentUser() reads _auth.currentUser. If Firebase has a cached session (from a previous run), the user is already signed in and we skip straight to the home screen.

This is a key UX improvement over showing a login screen to a user who's already logged in.

3. Sign-In Flow

When the button is tapped, _handleSignIn() runs through four steps:

  • _googleSignIn.signIn() — opens the system Google account picker. The user chooses an account, or cancels.
  • googleUser.authentication — retrieves the OAuth tokens (access token and ID token) for the chosen account. This is what Firebase needs to verify the identity.
  • GoogleAuthProvider.credential(...) — bundles those tokens into a credential object Firebase understands.
  • _auth.signInWithCredential(credential) — sends the credential to Firebase, which verifies it and creates (or retrieves) a Firebase user.

Once signed in, we pushReplacement to the home page — so the back button doesn't bring the user back to the login screen.

4. Sign-Out Flow

Logout is a two-step process. First we sign out of Google (clears the account picker cache), then sign out of Firebase (clears the session). If you skip either step, the user might be re-signed-in on the next launch or on a fresh Google sign-in attempt.

Common Pitfalls

  • Using outdated plugin versions. The original 2021 tutorial's versions won't compile against current Flutter. Always run flutter pub add to get the latest compatible versions.
  • Initialising Firebase in a widget's initState. It must be done once, in main(). Otherwise, hot restarts and widget rebuilds can cause "Firebase has already been initialised" errors.
  • Forgetting to enable Google Sign-In in the Firebase Console. The code compiles fine but the sign-in will fail at runtime with operation-not-allowed.
  • Missing SHA-1 fingerprint. On Android, Google Sign-In requires the app's SHA-1 to be registered in the Firebase project. Without it, you'll get a PlatformException(sign_in_failed, com.google.android.gms.common.api.ApiException: 10). Register the debug SHA-1 from ./gradlew signingReport and the release SHA-1 from your Play Store signing key.
  • Not handling the "user dismissed" case. If the user closes the account picker, signIn() returns null. Accessing googleUser.authentication without a null check crashes the app.
  • Only signing out of Google. The original code called googleSignIn.signOut() but forgot FirebaseAuth.instance.signOut(). The Firebase session persisted, so on next launch the user was still logged in.
  • Using Navigator.push instead of Navigator.pushReplacement. The login screen stays in the navigation stack, so pressing back returns the user to it.
  • Calling setState() after await without checking mounted. If the user navigates away during sign-in, calling setState() throws a "setState() called after dispose()" error. Always check if (mounted).
  • Calling context across async gaps. Flutter's linter warns about this. Use if (context.mounted) before any context-dependent call after an await.
  • Forgetting to add google-services.json. The Gradle plugin will fail at build time with a clear error, but this is easy to miss if you're copy-pasting tutorials.
  • Using JCenter. It was shut down in 2021. Remove it from your repositories and use mavenCentral().

Wrapping Up

You now have a complete Google Sign-In flow for Flutter: Firebase initialisation, automatic session restore, a working login button, and a home screen with proper logout. The code uses current plugin versions and handles the edge cases the original tutorial missed.

From here, you can extend the example with:

  • Email and password authentication alongside Google Sign-In, so users can choose how to log in.
  • Firestore integration — after sign-in, write the user's profile to Firestore so you can store app-specific data.
  • Anonymous authentication — let users try the app before signing in, then link their anonymous account to Google later.
  • StreamBuilder on authStateChanges() — reactively update the UI when the auth state changes, instead of manually navigating on sign-in/out.
  • Cross-platform support — for iOS, you'll need to add the URL scheme to Info.plist and register the client ID from Firebase. For web, google_sign_in works differently and needs additional setup.

You might also find these helpful:

No comments: