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.
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:
- Set up Supabase project and add the URL and anonymous key to your constants file.
- Use Supabase Authentication for user sign-up and sign-in.
- Implement image picking and upload functionality for user avatars.
- Store additional user information in a custom ‘users’ table in Supabase.
- 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.