Flutter app using Supabase as the backend: User Registration and Login

This article demonstrates how to implement user registration and login functionality in a Flutter app using Supabase as the backend. We’ll cover the project structure, package installation, and provide code examples for each component.

https://youtu.be/9M106GjU3BA

Project Structure

lib/
├── core/
│   └── constants/
│       └── constants.dart
├── models/
│   └── user_model.dart
├── screens/
│   ├── auth/
│   │   ├── login_screen.dart
│   │   └── register_screen.dart
│   ├── home/
│   │   └── home_screen.dart
│   └── splash_screen.dart
└── main.dart

Package Installation

Add the following packages to your pubspec.yaml file:

dependencies:
  supabase_flutter: ^2.6.0
  image_picker: ^1.1.2

Run flutter pub get to install the packages.

Supabase Configuration

Create a constants.dart file in the lib/core/constants/ directory and add your Supabase URL and anonymous key:

const SUPABASE_URL = "";
const SUPABASE_ANON_KEY = "";

Replace these values with your own Supabase project credentials.

Main Application

Create the main.dart file in the lib/ directory:

import 'package:flutter/material.dart';
import 'package:social_app/screens/splash_screen.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'core/constants/constants.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Supabase.initialize(
    url: SUPABASE_URL,
    anonKey: SUPABASE_ANON_KEY,
  );
  runApp(const MyApp());
}

final supabase = Supabase.instance.client;

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Social Mate',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const SplashScreen()
    );
  }
}

Splash Screen

Create the splash_screen.dart file in the lib/screens/ directory:

import 'package:flutter/material.dart';
import 'package:social_app/screens/auth/login_screen.dart';

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

  @override
  State<SplashScreen> createState() => _SplashScreenState();
}

class _SplashScreenState extends State<SplashScreen> {
  @override
  void initState() {
    super.initState();
    Future.delayed(const Duration(seconds: 3), () {
      Navigator.pushReplacement(
        context,
        MaterialPageRoute(builder: (context) => const LoginScreen()),
      );
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: Center(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Image.asset(
              'assets/images/bg_img.png',
              height: MediaQuery.of(context).size.height * 0.53,
              width: MediaQuery.of(context).size.width * 0.70,
            ),
            Image.asset(
              'assets/images/logo.png',
              height: MediaQuery.of(context).size.height * 0.3,
              width: MediaQuery.of(context).size.width * 0.70,
            ),
          ],
        ),
      ),
    );
  }
}

User Model

Create the user_model.dart file in the lib/models/ directory:

class UserModel {
  int? id;
  String? name;
  String? email;
  String? image;
  String? createdAt;

  UserModel({
    this.id,
    this.name,
    this.email,
    this.image,
    this.createdAt
  });

  Map<String, dynamic> toJson() {
    return {
      'name': name,
      'email': email,
      'image': image,
    };
  }

  factory UserModel.fromJson(Map<String, dynamic> json) {
    return UserModel(
      id: json['id'],
      name: json['name'],
      email: json['email'],
      image: json['image'],
      createdAt: DateTime.parse(json['created_at']).toString()
    );
  }
}

Login Screen

Create the login_screen.dart file in the lib/screens/auth/ directory:

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:social_app/main.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../home/home_screen.dart';
import 'register_screen.dart';

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

  @override
  _LoginScreenState createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  bool _isLoading = false;

  // Function to perform login
  Future<void> _loginUser() async {
    setState(() {
      _isLoading = true;
    });

    final email = _emailController.text;
    final password = _passwordController.text;

    if (email.isEmpty || password.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
        content: Text('Please fill in both fields'),
      ));
      setState(() {
        _isLoading = false;
      });
      return;
    }

    // Attempt to sign in with Supabase
    final response = await supabase.auth.signInWithPassword(
      email: email,
      password: password,
    );

    if (response.user == null) {
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
        content: Text('Login failed'),
      ));
    } else {
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
        content: Text('Login successful'),
      ));

       Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => const HomeScreen()));
    }

    setState(() {
      _isLoading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 16.0),
        child: Column(
          children: [
            const SizedBox(height: 60,),
            const Align(
              alignment: Alignment.centerLeft,
              child: Text(
                'Login',
                style: TextStyle(fontSize: 34, fontWeight: FontWeight.bold, color: Colors.black),
              ),
            ),
            const Align(
              alignment: Alignment.centerLeft,
              child: Text(
                'Please log in to continue',
                style: TextStyle(fontSize: 16, color: Colors.black),
              ),
            ),
            const SizedBox(height: 4,),
            Align(
              alignment: Alignment.centerLeft,
              child: Container(
                width: MediaQuery.of(context).size.width * 0.20,
                height: 5,
                decoration: BoxDecoration(
                  color: Colors.blue,
                  borderRadius: BorderRadius.circular(20)
                ),
              ),
            ),
            const SizedBox(height: 40,),
            Card(
              elevation: 5,
              color: Colors.white,
              child: Padding(
                padding: const EdgeInsets.all(8.0),
                child: Column(
                  children: [
                    TextField(
                      controller: _emailController,
                      keyboardType: TextInputType.emailAddress,
                      decoration: const InputDecoration(
                          labelText: 'Email',
                       border: OutlineInputBorder(
                         borderSide: BorderSide.none
                       )
                      ),
                    ),
                    TextField(
                      controller: _passwordController,
                      obscureText: true,
                      decoration: const InputDecoration(
                          labelText: 'Password',
                          border:
                              OutlineInputBorder(borderSide: BorderSide.none)
                      ),
                    ),
                    const SizedBox(height: 16,),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 20),
            _isLoading
                ? const CircularProgressIndicator()
                : Padding(
                  padding: const EdgeInsets.symmetric(horizontal: 8.0),
                  child: SizedBox(
                      width: double.infinity,
                      height: 45,
                      child: ElevatedButton(
                        onPressed: _loginUser,
                        style: ElevatedButton.styleFrom(
                          backgroundColor: Colors.blue,
                          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))
                        ),
                        child: const Text('Login', style: TextStyle(color: Colors.white),),
                      ),
                    ),
                ),
            const SizedBox(height: 14),
            RichText(
              text: TextSpan(
                children: [
                  const TextSpan(
                    text: "Don't have an account? ",
                    style: TextStyle(
                      fontSize: 16,
                      color: Colors.black, // Use your preferred color here
                    ),
                  ),
                  TextSpan(
                    text: 'Register here',
                    style: const TextStyle(
                      fontSize: 16,
                      color: Colors.blue, // Highlight the clickable text
                    ),
                    recognizer: TapGestureRecognizer()
                      ..onTap = () {
                        Navigator.push(
                          context,
                          MaterialPageRoute(builder: (context) => RegisterScreen()),
                        );
                      },
                  ),
                ],
              ),
            )
          ],
        ),
      ),
    );
  }
}

Register Screen

Create the register_screen.dart file in the lib/screens/auth/ directory:

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

import '../../main.dart';
import '../../models/user_model.dart';
import '../home/home_screen.dart';

class RegisterScreen extends StatefulWidget {
  @override
  _RegisterScreenState createState() => _RegisterScreenState();
}

class _RegisterScreenState extends State<RegisterScreen> {
  final _picker = ImagePicker();
  XFile? _image;
  final _nameController = TextEditingController();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();

  bool _isLoading = false;

  Future<void> _pickImage() async {
    try {
      final pickedFile = await _picker.pickImage(source: ImageSource.gallery);
      setState(() {
            _image = pickedFile;
      });
    } catch (e) {
      print(e);
    }
  }

  // Function to validate form inputs
  bool _validateInputs() {
    final name = _nameController.text;
    final email = _emailController.text;
    final password = _passwordController.text;

    if (_image == null || name.isEmpty || email.isEmpty || password.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
        content: Text('Please fill in all fields and select an image'),
      ));
      return false;
    }
    return true;
  }

  // Function to upload the image to Supabase Storage
  Future<String?> _uploadImage() async {
    final bytes = await _image!.readAsBytes();
    final fileExt = _image!.path.split('.').last;
    final fileName = '${DateTime.now().toIso8601String()}.$fileExt';
    final filePath = fileName;

    final storageResponse = await supabase.storage.from('images').uploadBinary(
      filePath,
      bytes,
      fileOptions: FileOptions(contentType: _image!.mimeType),
    );

    if (storageResponse.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
        content: Text('Image upload failed'),
      ));
      return null;
    }

    final imageUrl = supabase.storage
        .from('images')
        .getPublicUrl(filePath);
    return imageUrl;
  }

  // Function to sign up the user using Supabase Authentication
  Future<AuthResponse?> _signUpUser(String email, String password) async {
    final authResponse = await supabase.auth.signUp(
      email: email,
      password: password,
    );

    if (authResponse.user == null) {
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
        content: Text('Registration failed'),
      ));
      return null;
    }
    return authResponse;
  }

  // Function to save user data to Supabase 'users' table
  Future<void> _saveUserToDatabase(UserModel user) async {
    try {
      await supabase
          .from('users')
          .insert(user.toJson());

      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
        content: Text('User registered successfully'),
      ));

      Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => const HomeScreen()));

    } on PostgrestException catch (error) {
      ScaffoldMessenger.of(context).showSnackBar(SnackBar(
        content: Text('Registration failed: ${error.message}'),
      ));
    } catch (error) {
      ScaffoldMessenger.of(context).showSnackBar(SnackBar(
         content: Text('$error'),
      ));
    }
  }

   // Main function to register the user
  Future<void> _registerUser() async {
    setState(() {
      _isLoading = true;
    });

    // Step 1: Validate inputs
    if (!_validateInputs()) {
      setState(() {
        _isLoading = false;
      });
      return;
    }

    // Step 2: Upload image and get image URL
    final imageUrl = await _uploadImage();
    if (imageUrl == null) {
      setState(() {
        _isLoading = false;
      });
      return;
    }

    // Step 3: Register user with Supabase Auth
    final email = _emailController.text;
    final password = _passwordController.text;
    final authResponse = await _signUpUser(email, password);
    if (authResponse == null) {
      setState(() {
        _isLoading = false;
      });
      return;
    }

    // Step 4: Create UserModel and save to Supabase
    final newUser = UserModel(
      name: _nameController.text,
      email: email,
      image: imageUrl,
    );

    await _saveUserToDatabase(newUser);

    setState(() {
      _isLoading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 16.0),
        child: SingleChildScrollView(
          child: Column(
            children: [
              const SizedBox(height: 60),
              const Align(
                alignment: Alignment.centerLeft,
                child: Text(
                  'Register',
                  style: TextStyle(fontSize: 34, fontWeight: FontWeight.bold, color: Colors.black),
                ),
              ),
              const Align(
                alignment: Alignment.centerLeft,
                child: Text(
                  'Please sign up to continue',
                  style: TextStyle(fontSize: 16, color: Colors.black),
                ),
              ),
              const SizedBox(height: 4,),
              Align(
                alignment: Alignment.centerLeft,
                child: Container(
                  width: MediaQuery.of(context).size.width * 0.20,
                  height: 5,
                  decoration: BoxDecoration(
                      color: Colors.blue,
                      borderRadius: BorderRadius.circular(20)
                  ),
                ),
              ),
              const SizedBox(height: 40),
              GestureDetector(
                onTap: _pickImage,
                child: CircleAvatar(
                  radius: 50,
                  backgroundColor: Colors.grey[300],
                  backgroundImage: _image != null ? FileImage(File(_image!.path)) : null,
                  child: _image == null
                      ? const Icon(
                    Icons.camera_alt,
                    size: 40,
                    color: Colors.white,
                  )
                      : null,
                ),
              ),
              const SizedBox(height: 20),
              Card(
                elevation: 5,
                color: Colors.white,
                child: Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: Column(
                    children: [
                      TextField(
                        controller: _nameController,
                        decoration: const InputDecoration(
                            labelText: 'Name',
                            border:
                                OutlineInputBorder(borderSide: BorderSide.none),
                        ),
                      ),
                      TextField(
                        controller: _emailController,
                        decoration: const InputDecoration(labelText: 'Email',
                            border:
                            OutlineInputBorder(borderSide: BorderSide.none)),
                        keyboardType: TextInputType.emailAddress,
                      ),
                      TextField(
                        controller: _passwordController,
                        decoration: const InputDecoration(labelText: 'Password',
                            border:
                            OutlineInputBorder(borderSide: BorderSide.none)),
                        obscureText: true,
                      ),
                      const SizedBox(height: 16),
                    ],
                  ),
                ),
              ),
              const SizedBox(height: 20),
              _isLoading
                  ? const CircularProgressIndicator()
                  : Padding(
                      padding: const EdgeInsets.symmetric(horizontal: 8.0),
                      child: SizedBox(
                        width: double.infinity,
                        height: 45,
                        child: ElevatedButton(
                          onPressed: _registerUser,
                          style: ElevatedButton.styleFrom(
                              backgroundColor: Colors.blue,
                              shape: RoundedRectangleBorder(
                                  borderRadius: BorderRadius.circular(10))),
                          child: const Text(
                            'Register',
                            style: TextStyle(color: Colors.white),
                          ),
                        ),
                      ),
                    ),
            ],
          ),
        ),
      ),
    );
  }
}

Home Screen

Create the home_screen.dart file in the lib/screens/home/ directory:

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text('Home Screen'),
      ),
    );
  }
}

Conclusion

This article has demonstrated how to implement user registration and login functionality in a Flutter app using Supabase as the backend. We’ve covered the project structure, package installation, and provided code examples for each component.

Key points to remember:

  1. Set up Supabase project and add the URL and anonymous key to your constants file.
  2. Use Supabase Authentication for user sign-up and sign-in.
  3. Implement image picking and upload functionality for user avatars.
  4. Store additional user information in a custom ‘users’ table in Supabase.
  5. Handle errors and provide feedback to users during the registration and login processes.

With this implementation, you have a solid foundation for building a Flutter app with user authentication using Supabase. You can now expand on this base to add more features and functionalities to your app.

Leave a Comment