Supabase with Flutter: Mobile App Development Guide

Flutter and Supabase integration enables building cross-platform mobile applications for iOS and Android with a single codebase, leveraging Supabase's authentication, real-time database, storage, and backend services while utilizing Flutter's reactive UI framework for beautiful native experiences. Unlike separate native development requiring iOS and Android codebases, Flutter provides unified development with hot reload, while Supabase eliminates backend infrastructure setup offering authentication, database, file storage, and real-time features out of the box. This comprehensive guide covers setting up Supabase in Flutter projects, implementing authentication with email and social providers, building reactive database queries with StreamBuilder, handling real-time subscriptions for live updates, implementing image upload with storage, managing state with Provider or Riverpod, building offline-first apps with local caching, and deploying Flutter apps to App Store and Google Play. Flutter integration becomes ideal for startups building mobile-first products, developers wanting cross-platform development, teams with limited resources avoiding separate native teams, or projects requiring rapid prototyping and iteration. Before proceeding, understand JavaScript client basics, authentication, and real-time features.
Flutter Project Setup
# Create Flutter project
flutter create my_app
cd my_app
# Add Supabase package
flutter pub add supabase_flutter
# Add other useful packages
flutter pub add provider # State management
flutter pub add image_picker # Image selection
flutter pub add cached_network_image # Image caching
# pubspec.yaml dependencies:
# dependencies:
# flutter:
# sdk: flutter
# supabase_flutter: ^2.0.0
# provider: ^6.1.1
# image_picker: ^1.0.5
# cached_network_image: ^3.3.0
# Project structure:
# lib/
# main.dart
# models/
# post.dart
# providers/
# auth_provider.dart
# posts_provider.dart
# screens/
# auth_screen.dart
# home_screen.dart
# post_detail_screen.dart
# services/
# supabase_service.dart
# widgets/
# post_card.dartInitialize Supabase Client
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:provider/provider.dart';
import 'providers/auth_provider.dart';
import 'screens/auth_screen.dart';
import 'screens/home_screen.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Supabase.initialize(
url: 'YOUR_SUPABASE_URL',
anonKey: 'YOUR_SUPABASE_ANON_KEY',
);
runApp(const MyApp());
}
final supabase = Supabase.instance.client;
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => AuthProvider()),
],
child: MaterialApp(
title: 'Supabase Flutter App',
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
),
home: const AuthCheck(),
),
);
}
}
class AuthCheck extends StatelessWidget {
const AuthCheck({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return StreamBuilder<AuthState>(
stream: supabase.auth.onAuthStateChange,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
final session = snapshot.hasData ? snapshot.data!.session : null;
if (session != null) {
return const HomeScreen();
} else {
return const AuthScreen();
}
},
);
}
}Authentication Provider
// lib/providers/auth_provider.dart
import 'package:flutter/foundation.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../main.dart';
class AuthProvider with ChangeNotifier {
User? _user;
bool _loading = false;
String? _error;
User? get user => _user;
bool get loading => _loading;
String? get error => _error;
AuthProvider() {
_user = supabase.auth.currentUser;
supabase.auth.onAuthStateChange.listen((data) {
_user = data.session?.user;
notifyListeners();
});
}
Future<void> signUp(String email, String password) async {
_loading = true;
_error = null;
notifyListeners();
try {
await supabase.auth.signUp(
email: email,
password: password,
);
} on AuthException catch (e) {
_error = e.message;
} catch (e) {
_error = 'An unexpected error occurred';
} finally {
_loading = false;
notifyListeners();
}
}
Future<void> signIn(String email, String password) async {
_loading = true;
_error = null;
notifyListeners();
try {
await supabase.auth.signInWithPassword(
email: email,
password: password,
);
} on AuthException catch (e) {
_error = e.message;
} catch (e) {
_error = 'An unexpected error occurred';
} finally {
_loading = false;
notifyListeners();
}
}
Future<void> signInWithGoogle() async {
_loading = true;
_error = null;
notifyListeners();
try {
await supabase.auth.signInWithOAuth(
Provider.google,
redirectTo: 'io.supabase.flutterquickstart://login-callback/',
);
} on AuthException catch (e) {
_error = e.message;
} finally {
_loading = false;
notifyListeners();
}
}
Future<void> signOut() async {
await supabase.auth.signOut();
}
}Authentication Screen
// lib/screens/auth_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/auth_provider.dart';
class AuthScreen extends StatefulWidget {
const AuthScreen({Key? key}) : super(key: key);
@override
State<AuthScreen> createState() => _AuthScreenState();
}
class _AuthScreenState extends State<AuthScreen> {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _isSignUp = false;
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final authProvider = Provider.of<AuthProvider>(context);
return Scaffold(
appBar: AppBar(
title: Text(_isSignUp ? 'Sign Up' : 'Sign In'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 16),
TextField(
controller: _passwordController,
decoration: const InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(),
),
obscureText: true,
),
const SizedBox(height: 24),
if (authProvider.error != null)
Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Text(
authProvider.error!,
style: const TextStyle(color: Colors.red),
),
),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: authProvider.loading
? null
: () async {
if (_isSignUp) {
await authProvider.signUp(
_emailController.text,
_passwordController.text,
);
} else {
await authProvider.signIn(
_emailController.text,
_passwordController.text,
);
}
},
child: authProvider.loading
? const CircularProgressIndicator()
: Text(_isSignUp ? 'Sign Up' : 'Sign In'),
),
),
const SizedBox(height: 16),
TextButton(
onPressed: () {
setState(() {
_isSignUp = !_isSignUp;
});
},
child: Text(
_isSignUp
? 'Already have an account? Sign In'
: "Don't have an account? Sign Up",
),
),
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 16),
OutlinedButton.icon(
onPressed: authProvider.loading
? null
: () => authProvider.signInWithGoogle(),
icon: const Icon(Icons.login),
label: const Text('Sign in with Google'),
),
],
),
),
);
}
}Database Queries with StreamBuilder
// lib/models/post.dart
class Post {
final String id;
final String title;
final String content;
final String userId;
final DateTime createdAt;
Post({
required this.id,
required this.title,
required this.content,
required this.userId,
required this.createdAt,
});
factory Post.fromMap(Map<String, dynamic> map) {
return Post(
id: map['id'],
title: map['title'],
content: map['content'],
userId: map['user_id'],
createdAt: DateTime.parse(map['created_at']),
);
}
Map<String, dynamic> toMap() {
return {
'title': title,
'content': content,
};
}
}
// lib/screens/home_screen.dart
import 'package:flutter/material.dart';
import '../main.dart';
import '../models/post.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Posts'),
actions: [
IconButton(
icon: const Icon(Icons.logout),
onPressed: () => supabase.auth.signOut(),
),
],
),
body: StreamBuilder<List<Map<String, dynamic>>>(
stream: supabase
.from('posts')
.stream(primaryKey: ['id'])
.order('created_at', ascending: false),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final posts = snapshot.data!
.map((data) => Post.fromMap(data))
.toList();
if (posts.isEmpty) {
return const Center(child: Text('No posts yet'));
}
return ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) {
final post = posts[index];
return Card(
margin: const EdgeInsets.all(8),
child: ListTile(
title: Text(post.title),
subtitle: Text(post.content),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () => _deletePost(post.id),
),
),
);
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showCreatePostDialog(context),
child: const Icon(Icons.add),
),
);
}
Future<void> _deletePost(String id) async {
await supabase.from('posts').delete().eq('id', id);
}
void _showCreatePostDialog(BuildContext context) {
final titleController = TextEditingController();
final contentController = TextEditingController();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Create Post'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: titleController,
decoration: const InputDecoration(labelText: 'Title'),
),
TextField(
controller: contentController,
decoration: const InputDecoration(labelText: 'Content'),
maxLines: 3,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () async {
await supabase.from('posts').insert({
'title': titleController.text,
'content': contentController.text,
});
if (context.mounted) Navigator.pop(context);
},
child: const Text('Create'),
),
],
),
);
}
}Image Upload with Storage
// lib/services/storage_service.dart
import 'dart:io';
import 'package:image_picker/image_picker.dart';
import '../main.dart';
class StorageService {
final ImagePicker _picker = ImagePicker();
Future<String?> uploadImage() async {
try {
// Pick image
final XFile? image = await _picker.pickImage(
source: ImageSource.gallery,
maxWidth: 1024,
maxHeight: 1024,
imageQuality: 85,
);
if (image == null) return null;
// Generate unique filename
final userId = supabase.auth.currentUser!.id;
final timestamp = DateTime.now().millisecondsSinceEpoch;
final fileName = '$userId/$timestamp.${image.path.split('.').last}';
// Upload to Supabase Storage
final file = File(image.path);
await supabase.storage.from('avatars').upload(
fileName,
file,
fileOptions: const FileOptions(
upsert: true,
),
);
// Get public URL
final publicUrl = supabase.storage
.from('avatars')
.getPublicUrl(fileName);
return publicUrl;
} catch (e) {
print('Error uploading image: $e');
return null;
}
}
Future<void> deleteImage(String url) async {
try {
// Extract path from URL
final uri = Uri.parse(url);
final path = uri.pathSegments.last;
await supabase.storage.from('avatars').remove([path]);
} catch (e) {
print('Error deleting image: $e');
}
}
}
// Usage in widget
import 'package:cached_network_image/cached_network_image.dart';
class ProfileImageWidget extends StatefulWidget {
const ProfileImageWidget({Key? key}) : super(key: key);
@override
State<ProfileImageWidget> createState() => _ProfileImageWidgetState();
}
class _ProfileImageWidgetState extends State<ProfileImageWidget> {
final _storageService = StorageService();
String? _imageUrl;
bool _uploading = false;
Future<void> _uploadImage() async {
setState(() => _uploading = true);
final url = await _storageService.uploadImage();
if (url != null) {
setState(() {
_imageUrl = url;
_uploading = false;
});
} else {
setState(() => _uploading = false);
}
}
@override
Widget build(BuildContext context) {
return Column(
children: [
if (_imageUrl != null)
CachedNetworkImage(
imageUrl: _imageUrl!,
width: 150,
height: 150,
fit: BoxFit.cover,
placeholder: (context, url) => const CircularProgressIndicator(),
errorWidget: (context, url, error) => const Icon(Icons.error),
)
else
Container(
width: 150,
height: 150,
color: Colors.grey[300],
child: const Icon(Icons.person, size: 50),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _uploading ? null : _uploadImage,
child: _uploading
? const CircularProgressIndicator()
: const Text('Upload Image'),
),
],
);
}
}Flutter + Supabase Best Practices
- Use StreamBuilder: Leverage StreamBuilder with Supabase streams for real-time UI updates
- State Management: Use Provider, Riverpod, or Bloc for managing auth and app state
- Error Handling: Catch AuthException and handle errors gracefully with user feedback
- Dispose Controllers: Always dispose TextEditingControllers and other resources
- Image Optimization: Compress images before upload using maxWidth and imageQuality
- Offline Support: Implement local caching with packages like hive or drift
- Deep Linking: Configure deep links for OAuth callbacks and password resets
Common Issues
- OAuth Not Working: Configure deep linking in android/app/src/main/AndroidManifest.xml and ios/Runner/Info.plist
- Stream Not Updating: Ensure RLS policies allow user to view data and real-time is enabled
- Image Upload Fails: Check storage bucket permissions and file size limits
- Context Errors: Use context.mounted checks before Navigator operations after async calls
Next Steps
- Learn Auth Patterns: Explore email authentication and OAuth providers
- Secure Your App: Implement Row Level Security policies
- Compare Frameworks: Explore React or Next.js for web
- Publish Apps: Deploy to App Store and Google Play with proper environment configs
Conclusion
Flutter and Supabase integration enables building cross-platform mobile applications for iOS and Android with single codebase leveraging Supabase's authentication, database, storage, and real-time features while utilizing Flutter's reactive framework for beautiful native experiences. By implementing authentication providers with ChangeNotifier for state management, using StreamBuilder with Supabase streams for real-time UI updates, handling image uploads with storage bucket integration, and managing errors gracefully with try-catch blocks, you create production-ready mobile apps. Flutter's hot reload accelerates development, StreamBuilder provides automatic UI updates when data changes, and Supabase eliminates backend infrastructure requirements offering complete backend-as-a-service. Always use Provider or Riverpod for state management, dispose controllers and resources properly, handle authentication errors with meaningful messages, optimize images before upload, implement offline support for better UX, and configure deep linking for OAuth flows. Flutter + Supabase combination provides excellent developer experience for building modern mobile applications with minimal backend complexity. Continue learning with TypeScript integration, security policies, and database migrations.
$ share --platform
$ cat /comments/ (0)
$ cat /comments/
// No comments found. Be the first!


