Supabase Login With Flutter: A Quick Guide

by Alex Braham 43 views

H Hey guys! So, you're diving into the awesome world of Flutter and want to integrate Supabase for your backend needs, right? And naturally, the first hurdle is getting your users logged in. Well, you've come to the right place! In this article, we're going to break down how to implement Supabase login in your Flutter app, making it super straightforward. We'll cover everything from setting up your Supabase project to writing the actual Flutter code that handles user authentication. Get ready to level up your app development game!

Getting Started with Supabase and Flutter

Before we even think about writing a single line of login code, we need to make sure our development environment is all set up. Supabase is an open-source Firebase alternative, and it’s incredibly powerful. It provides a PostgreSQL database, authentication, instant APIs, storage, and more, all with a generous free tier to get you started. For Flutter, integrating Supabase is a breeze thanks to the official supabase_flutter SDK. First things first, you’ll need to create a Supabase account if you haven’t already. Head over to supabase.com and sign up. Once you’re in, create a new project. Give it a cool name and choose a region close to you for better performance. After your project is created, you’ll find your Project URL and anon public key on the API settings page. Keep these handy – you’ll need them to connect your Flutter app to your Supabase backend.

Now, let's talk Flutter. Make sure you have Flutter installed and set up correctly on your machine. Open your Flutter project (or create a new one if you’re starting fresh). The next crucial step is to add the supabase_flutter package to your pubspec.yaml file. You can find the latest version on pub.dev. Add it under dependencies:

dependencies:
  flutter:
    sdk: flutter
  supabase_flutter:
    # Use the latest version from pub.dev
    version: "^1.8.0" # or any other recent version
  # ... other dependencies

After adding the dependency, run flutter pub get in your terminal to download and install the package. This SDK is the bridge that connects your Flutter application to your Supabase project, enabling all sorts of cool features, including authentication.

Setting Up Your Supabase Project for Authentication

Alright, let’s get back to Supabase. To enable user authentication in your Flutter app, you need to configure it within your Supabase project dashboard. Navigate to the Authentication section in your Supabase project. Here, you'll find various providers like Email/Password, Google, GitHub, and more. For a basic login system, Email/Password authentication is the way to go. Make sure it’s enabled. You can also configure email templates for sign-up confirmation, password resets, and more, which is super handy for a polished user experience.

It’s also extremely important to configure your Auth Redirect URLs. When a user signs up or logs in, Supabase might redirect them back to your app. You need to tell Supabase which URLs are valid for these redirects. For development, you’ll typically use a local URL like http://localhost:port (though this depends on your Flutter setup and how you’re running the app). For production, you’ll use your app’s domain. You can add multiple URLs here. Under the “Site URL” section in your Supabase project’s authentication settings, add your development URL. This prevents security issues and ensures that redirects only go to trusted locations. Without this step, your authentication flow might break, and users won't be able to complete the sign-up or login process. So, don't skip this part, guys!

Finally, you’ll need to set up your database. For a simple authentication system, you might not need much, but it’s good practice to create a users table if you plan to store additional user information beyond what Supabase’s built-in auth.users table provides. You can do this using the SQL Editor in your Supabase dashboard. A simple CREATE TABLE IF NOT EXISTS public.users (...) statement will do the trick. Later, you can link this table to the auth.users table using the id column. This separation helps keep your data organized and allows you to add custom fields like username, avatar_url, etc., specific to your application's needs. This structure is foundational for building a robust user management system on top of Supabase. Remember to enable Row Level Security (RLS) on your tables for data security – it’s a critical aspect of Supabase.

Implementing Supabase Email/Password Authentication in Flutter

Now for the fun part – writing the Flutter code! Implementing Supabase login in Flutter involves a few key steps: initializing the Supabase client, creating UI forms for sign-up and login, and then using the supabase_flutter SDK to interact with Supabase's authentication functions. Let’s start with initializing the Supabase client. In your main.dart file (or wherever you initialize your app), you need to set up the Supabase client using the Project URL and anon public key you obtained earlier. This is typically done in your main() function before runApp():

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

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Supabase.initialize(
    url: 'YOUR_SUPABASE_URL',
    anonKey: 'YOUR_SUPABASE_ANON_KEY',
  );

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    // Your app structure here
    return MaterialApp(
      title: 'Supabase Auth Demo',
      home: LoginPage(), // We'll create this widget
    );
  }
}

Remember to replace 'YOUR_SUPABASE_URL' and 'YOUR_SUPABASE_ANON_KEY' with your actual Supabase credentials. It’s a good practice to store these sensitive keys securely, perhaps using environment variables or a configuration file, rather than hardcoding them directly in your code, especially for production builds.

Building the UI for Login and Sign-up

Next, we need to create the user interface for our login and sign-up screens. This usually involves TextField widgets for email and password, and ElevatedButton widgets to trigger the respective actions. Let’s create a simple LoginPage widget. This page will handle both login and sign-up flows, toggling between them with a button.

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

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

  @override
  State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  final _formKey = GlobalKey<FormState>();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  bool _isLogin = true;
  bool _isLoading = false;

  Future<void> _submit() async {
    if (!_formKey.currentState!.validate()) {
      return;
    }
    setState(() {
      _isLoading = true;
    });

    try {
      if (_isLogin) {
        // Login
        final AuthResponse response = await Supabase.instance.client.auth.signInWithPassword(
          email: _emailController.text,
          password: _passwordController.text,
        );
        // Handle successful login (e.g., navigate to home screen)
        ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Logged in successfully!')));
        print('Login successful: ${response.user?.email}');
      } else {
        // Sign up
        final AuthResponse response = await Supabase.instance.client.auth.signUp(
          email: _emailController.text,
          password: _passwordController.text,
        );
        // Handle successful sign-up (e.g., send verification email, navigate to a pending screen)
        ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Sign up successful! Check your email.')));
        print('Sign up successful: ${response.user?.email}');
        // Optionally, you can ask user to verify email before logging in
        // await Supabase.instance.client.auth.signOut(); // Sign out after signup if verification is needed
      }
    } catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error: ${e.toString()}')));
      print('Authentication error: $e');
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(_isLogin ? 'Login' : 'Sign Up')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Form(
          key: _formKey,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              TextFormField(
                controller: _emailController,
                decoration: const InputDecoration(labelText: 'Email'),
                keyboardType: TextInputType.emailAddress,
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Please enter your email';
                  }
                  return null;
                },
              ),
              const SizedBox(height: 16),
              TextFormField(
                controller: _passwordController,
                decoration: const InputDecoration(labelText: 'Password'),
                obscureText: true,
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Please enter your password';
                  }
                  return null;
                },
              ),
              const SizedBox(height: 24),
              if (_isLoading)
                const CircularProgressIndicator()
              else
                ElevatedButton(
                  onPressed: _submit,
                  child: Text(_isLogin ? 'Login' : 'Sign Up'),
                ),
              const SizedBox(height: 16),
              TextButton(
                onPressed: () {
                  setState(() {
                    _isLogin = !_isLogin;
                    _emailController.clear();
                    _passwordController.clear();
                  });
                },
                child: Text(_isLogin ? 'Need an account? Sign Up' : 'Already have an account? Login'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

This LoginPage widget provides a basic form. The _submit function handles the logic for either logging in or signing up based on the _isLogin boolean. We use ScaffoldMessenger to display feedback to the user (success or error messages) and a CircularProgressIndicator to show that an action is in progress. Pretty neat, huh?

Handling Authentication State and User Sessions

Once a user is logged in or signed up, you'll want to manage their session and update your app's UI accordingly. Supabase provides a listener for authentication state changes that you can use to automatically update your app's navigation or UI. This is crucial for providing a seamless user experience. You can set up a listener in your main.dart or a dedicated auth service class.

// In your main.dart or an auth service class
void setupAuthListener() {
  Supabase.instance.client.auth.onAuthStateChange.listen((AuthStateChange state) {
    final event = state.event;
    final session = state.session;

    if (event == AuthChangeEvent.signedIn) {
      // User is signed in. Navigate to the home screen.
      print('Auth state changed: Signed In');
      // You might want to navigate to your main app screen here
      // Navigator.of(context).pushReplacementNamed('/home'); // Example navigation
    } else if (event == AuthChangeEvent.signedOut) {
      // User is signed out. Navigate to the login screen.
      print('Auth state changed: Signed Out');
      // Navigator.of(context).pushReplacementNamed('/login'); // Example navigation
    } else if (event == AuthChangeEvent.userChanged) {
      // User details changed
      print('Auth state changed: User Changed');
    } else if (event == AuthChangeEvent.passwordRecovery) {
      // Password recovery process started
      print('Auth state changed: Password Recovery');
    }
  });
}

// Call this function after Supabase.initialize() in your main function
// setupAuthListener();

This listener ensures that if a user's authentication status changes (e.g., they log out from another device, or their session expires), your Flutter app reacts accordingly. You can use the session object to get information about the currently logged-in user, like their ID and access token. For managing the app's root screen (e.g., showing LoginPage or HomePage), you can create a root widget that checks the current auth state upon app startup and navigates to the appropriate screen. Supabase automatically stores the session token locally, so when the app restarts, it can often resume the user's session if the token is still valid. This is a fundamental aspect of user session management in Flutter apps using Supabase.

Advanced Authentication Features with Supabase Flutter

Supabase doesn't just stop at basic email/password login. It offers a plethora of advanced authentication features that you can easily integrate into your Flutter app. These include social logins (Google, GitHub, etc.), magic links, multi-factor authentication (MFA), and more. Let’s briefly touch upon some of these.

Social Logins (Google, GitHub, etc.)

Integrating social logins is a fantastic way to improve user experience. Users love the convenience of signing up or logging in with their existing social accounts. To enable this, you first need to configure the desired social provider in your Supabase project dashboard under Authentication -> Providers. For example, to enable Google Sign-In, you’ll need to create OAuth credentials in the Google Cloud Console and add them to your Supabase settings. Once configured, using them in Flutter is straightforward. You’ll use methods like signInWithProvider(OAuthProvider.google) from the supabase_flutter SDK.

// Example for Google Sign-In
Future<void> _signInWithGoogle() async {
  try {
    final AuthResponse response = await Supabase.instance.client.auth.signInWithOAuth(
      provider: OAuthProvider.google, // or OAuthProvider.github, etc.
      // The redirectTo parameter is crucial for web and desktop apps
      // It should match one of your configured Site URLs in Supabase Auth settings
      redirectTo: 'YOUR_REDIRECT_URL', // e.g., 'io.supabase.flutter://callback/' or 'http://localhost:3000/callback'
    );
    // For mobile, the SDK often handles the redirect automatically.
    // For web, you might need to handle the redirect in your web app.
    print('Google Sign-In successful');
    // Navigate to home screen or update UI
  } catch (e) {
    print('Google Sign-In error: $e');
    // Show error message to user
  }
}

Note: The redirectTo URL is critical. For mobile apps, Supabase often uses a deep linking scheme (e.g., io.supabase.flutter://callback/). For web apps, it should be a URL accessible by your web server. Make sure this URL is correctly configured in your Supabase project's authentication settings. This allows Supabase to redirect the user back to your app after they've authenticated with the social provider. This is a key part of the OAuth flow.

Password Resets and Email Verification

Supabase also simplifies common authentication flows like password resets and email verification. When a user signs up using signUp(), Supabase can automatically send a verification email. You can customize these email templates in your Supabase dashboard. For password resets, you can use resetPasswordForEmail() which sends a password reset link to the user's email. The user clicks the link, which takes them to a Supabase-hosted page (or your custom page if configured) where they can set a new password. Your app can then capture the new password using the token from the URL.

// Request password reset
Future<void> _requestPasswordReset(String email) async {
  try {
    await Supabase.instance.client.auth.resetPasswordForEmail(email);
    ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Password reset email sent!')));
  } catch (e) {
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error sending reset email: ${e.toString()}')));
  }
}

// Example of handling deep links for password reset (simplified)
// This would typically involve a deep linking package and handling the URL
Future<void> handlePasswordResetDeepLink(Uri uri) async {
  final String? token = uri.queryParameters['token'];
  if (token != null) {
    // Navigate to a screen where the user can enter a new password
    // Pass the token to this screen
    print('Password reset token: $token');
    // Example: Navigator.push(context, MaterialPageRoute(builder: (context) => NewPasswordPage(token: token)));
  }
}

These features significantly enhance the security and usability of your application, providing standard authentication mechanisms that users are familiar with. Implementing these requires careful handling of deep links and user feedback to guide them through the process.

Best Practices for Supabase Authentication in Flutter

To wrap things up, let’s go over some best practices when implementing Supabase login in your Flutter app. Following these guidelines will help you build a more secure, robust, and user-friendly authentication system.

  • Security First: Never hardcode your Supabase URL and anon key directly in your app's source code, especially if you plan to release it publicly. Use environment variables or a secure configuration management system. Also, always enable Row Level Security (RLS) on your Supabase database tables to protect your data.
  • User Experience: Provide clear feedback to users during the authentication process. Use loading indicators, informative error messages, and clear calls to action. Make the sign-up and login forms intuitive and easy to use. Offer social logins if appropriate for your user base.
  • Error Handling: Implement comprehensive error handling for all authentication operations. Supabase throws specific exceptions that you can catch and handle gracefully, informing the user about what went wrong (e.g., invalid email, weak password, user already exists).
  • Session Management: Utilize Supabase's onAuthStateChange stream to automatically manage user sessions and update your app's UI accordingly. Ensure users are automatically logged out or prompted to re-authenticate when their session expires.
  • State Management: Use a robust state management solution (like Provider, Riverpod, BLoC, or GetX) in your Flutter app to manage the authentication state effectively. This helps in cleanly reflecting the user's logged-in or logged-out status throughout the application.
  • Testing: Thoroughly test your authentication flow, including sign-up, login, password reset, and social logins, on different devices and platforms. Test edge cases like empty inputs, invalid credentials, and network errors.

By keeping these practices in mind, you'll be well on your way to building a top-notch authentication system for your Flutter application using Supabase. It’s a powerful combination that offers flexibility and ease of use. So go ahead, implement that Supabase login Flutter feature, and build something amazing!