diff --git a/web/docs/guides/with-flutter.mdx b/web/docs/guides/with-flutter.mdx
index 5718abc348a..47b92cb3343 100644
--- a/web/docs/guides/with-flutter.mdx
+++ b/web/docs/guides/with-flutter.mdx
@@ -19,7 +19,7 @@ This example provides the steps to build a simple user management app (from scra
By the end of this guide you'll have an app which allows users to login and update some basic profile details:
-
+
### Github
@@ -99,12 +99,8 @@ commit;
alter publication supabase_realtime add table profiles;
-- Set up Storage!
-insert into storage.buckets (id, name)
-values ('avatars', 'avatars');
-
-create policy "Avatar images are publicly accessible."
- on storage.objects for select
- using ( bucket_id = 'avatars' );
+insert into storage.buckets (id, name, public)
+values ('avatars', 'avatars', true);
create policy "Anyone can upload an avatar."
on storage.objects for insert
@@ -154,12 +150,12 @@ an app called `supabase_quickstart`:
flutter create supabase_quickstart
```
-Then let's install the only additional dependency: [supabase-flutter](https://github.com/supabase/supabase-flutter)
+Then let's install the only additional dependency: [`supabase_flutter`](https://github.com/supabase/supabase-flutter)
-Open the app and copy and paste the following inside `pubspec.yaml` under `dependencies`.
+Run the following command to get the newest version of `supabase_flutter` to your project.
-```yaml
-supabase_flutter: ^0.1.0
+```bash
+flutter pub add supabase_flutter
```
Run `flutter pub get` to install the dependencies.
@@ -260,6 +256,7 @@ In order to handle deep links for Android and iOS, let's create a class that wil
import 'package:flutter/material.dart';
import 'package:supabase/supabase.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
+import 'package:supabase_quickstart/utils/constants.dart';
class AuthState extends SupabaseAuthState {
@override
@@ -272,7 +269,8 @@ class AuthState extends SupabaseAuthState {
@override
void onAuthenticated(Session session) {
if (mounted) {
- Navigator.of(context).pushNamedAndRemoveUntil('/account', (route) => false);
+ Navigator.of(context)
+ .pushNamedAndRemoveUntil('/account', (route) => false);
}
}
@@ -281,7 +279,7 @@ class AuthState extends SupabaseAuthState {
@override
void onErrorAuthenticating(String message) {
- print('Error authenticating $message');
+ context.showErrorSnackBar(message: message);
}
}
```
@@ -306,12 +304,30 @@ class AuthRequiredState
}
```
-Let's also create a constant file to make it easier to use Supabase client.
+Let's also create a constant file to make it easier to use Supabase client.
+We will also include an extension method delaration to call `showSnackBar` with one line of code.
```dart title="lib/utils/constants.dart"
+import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
final supabase = Supabase.instance.client;
+
+extension ShowSnackBar on BuildContext {
+ void showSnackBar({
+ required String message,
+ Color backgroundColor = Colors.white,
+ }) {
+ ScaffoldMessenger.of(this).showSnackBar(SnackBar(
+ content: Text(message),
+ backgroundColor: backgroundColor,
+ ));
+ }
+
+ void showErrorSnackBar({required String message}) {
+ showSnackBar(message: message, backgroundColor: Colors.red);
+ }
+}
```
### Set up Splash Screen
@@ -321,7 +337,7 @@ This splash screen inherits `AuthState` to redirect users to the appropriate pag
```dart title="lib/pages/splash_page.dart"
import 'package:flutter/material.dart';
-import 'package:supabase_flutter_guide/components/auth_state.dart';
+import 'package:supabase_quickstart/components/auth_state.dart';
class SplashPage extends StatefulWidget {
const SplashPage({Key? key}) : super(key: key);
@@ -355,9 +371,9 @@ This page will also inherit `AuthState` as it will handle user login.
```dart title="lib/pages/login_page.dart"
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
-import 'package:supabase_flutter_guide/components/auth_state.dart';
import 'package:supabase/supabase.dart';
-import 'package:supabase_flutter_guide/utils/constants.dart';
+import 'package:supabase_quickstart/components/auth_state.dart';
+import 'package:supabase_quickstart/utils/constants.dart';
class LoginPage extends StatefulWidget {
const LoginPage({Key? key}) : super(key: key);
@@ -380,17 +396,15 @@ class _LoginPageState extends AuthState {
redirectTo: kIsWeb
? null
: 'io.supabase.flutterquickstart://login-callback/'));
- if (response.error != null) {
- ScaffoldMessenger.of(context).showSnackBar(SnackBar(
- content: Text(response.error!.message),
- backgroundColor: Colors.red,
- ));
+ final error = response.error;
+ if (error != null) {
+ context.showErrorSnackBar(message: error.message);
} else {
- ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(content: Text('Check your email for login link!')));
- }
- setState(() {
+ context.showSnackBar(message: 'Check your email for login link!');
_emailController.clear();
+ }
+
+ setState(() {
_isLoading = false;
});
}
@@ -440,9 +454,9 @@ Note that this page will inherit `AuthRequiredState` as user needs to be authent
```dart title="lib/pages/account_page.dart"
import 'package:flutter/material.dart';
-import 'package:supabase_flutter_guide/components/auth_required_state.dart';
-import 'package:supabase_flutter_guide/utils/constants.dart';
import 'package:supabase/supabase.dart';
+import 'package:supabase_quickstart/components/auth_required_state.dart';
+import 'package:supabase_quickstart/utils/constants.dart';
class AccountPage extends StatefulWidget {
const AccountPage({Key? key}) : super(key: key);
@@ -452,10 +466,11 @@ class AccountPage extends StatefulWidget {
}
class _AccountPageState extends AuthRequiredState {
- late final _usernameController = TextEditingController();
- late final _websiteController = TextEditingController();
+ final _usernameController = TextEditingController();
+ final _websiteController = TextEditingController();
var _loading = false;
+ /// Called once a user id is received within `onAuthenticated()`
Future _getProfile(String userId) async {
setState(() {
_loading = true;
@@ -466,19 +481,21 @@ class _AccountPageState extends AuthRequiredState {
.eq('id', userId)
.single()
.execute();
- if (response.error != null && response.status != 406) {
- ScaffoldMessenger.of(context)
- .showSnackBar(SnackBar(content: Text(response.error!.message)));
+ final error = response.error;
+ if (error != null && response.status != 406) {
+ context.showErrorSnackBar(message: error.message);
}
- if (response.data != null) {
- _usernameController.text = response.data!['username'] as String;
- _websiteController.text = response.data!['website'] as String;
+ final data = response.data;
+ if (data != null) {
+ _usernameController.text = (data['username'] ?? '') as String;
+ _websiteController.text = (data['website'] ?? '') as String;
}
setState(() {
_loading = false;
});
}
+ /// Called when user taps `Update` button
Future _updateProfile() async {
setState(() {
_loading = true;
@@ -493,14 +510,11 @@ class _AccountPageState extends AuthRequiredState {
'updated_at': DateTime.now().toIso8601String(),
};
final response = await supabase.from('profiles').upsert(updates).execute();
- if (response.error != null) {
- ScaffoldMessenger.of(context).showSnackBar(SnackBar(
- content: Text(response.error!.message),
- backgroundColor: Colors.red,
- ));
+ final error = response.error;
+ if (error != null) {
+ context.showErrorSnackBar(message: error.message);
} else {
- ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(content: Text('Successfully updated profile!')));
+ context.showSnackBar(message: 'Successfully updated profile!');
}
setState(() {
_loading = false;
@@ -509,11 +523,9 @@ class _AccountPageState extends AuthRequiredState {
Future _signOut() async {
final response = await supabase.auth.signOut();
- if (response.error != null) {
- ScaffoldMessenger.of(context).showSnackBar(SnackBar(
- content: Text(response.error!.message),
- backgroundColor: Colors.red,
- ));
+ final error = response.error;
+ if (error != null) {
+ context.showErrorSnackBar(message: error.message);
}
Navigator.of(context).pushReplacementNamed('/login');
}
@@ -574,16 +586,17 @@ Now that we have all the components in place, let's update `lib/main.dart`:
```dart title="lib/main.dart"
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
-import 'package:supabase_flutter_guide/pages/account_page.dart';
-import 'package:supabase_flutter_guide/pages/login_page.dart';
-import 'package:supabase_flutter_guide/pages/splash_page.dart';
+import 'package:supabase_quickstart/pages/account_page.dart';
+import 'package:supabase_quickstart/pages/login_page.dart';
+import 'package:supabase_quickstart/pages/splash_page.dart';
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
await Supabase.initialize(
+ // TODO: Replace credentials with your own
url: '[YOUR_SUPABASE_URL]',
- anonKey: '[YOUR_SUPABASE_ANON_KEY]',
+ anonKey: '[YOUR_SUPABASE_ANNON_KEY]',
);
runApp(MyApp());
}
@@ -595,7 +608,6 @@ class MyApp extends StatelessWidget {
title: 'Supabase Flutter',
theme: ThemeData.dark().copyWith(
primaryColor: Colors.green,
- accentColor: Colors.green,
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
onPrimary: Colors.white,
@@ -628,7 +640,276 @@ flutter run -d web-server --web-hostname localhost --web-port 3000
And then open the browser to [localhost:3000](http://localhost:3000) and you should see the completed app.
-
+
+
+
+## Bonus: Profile photos
+
+Every Supabase project is configured with [Storage](/docs/guides/storage) for managing large files like
+photos and videos.
+
+### Adding image uploading feature to Account page
+
+We will use [`image_picker`](https://pub.dev/packages/image_picker) plugin to select an image form the device.
+
+Run the following command to install it.
+
+```bash
+flutter pub add image_picker
+```
+
+Using [`image_picker`](https://pub.dev/packages/image_picker) requires some additional preparation depending on the platform.
+Follow the instruction on README.md of [`image_picker`](https://pub.dev/packages/image_picker) on how to set it up for the platform you are using.
+
+Once you are done with all of the above, it is time to dive into coding.
+
+### Create an upload widget
+
+Let's create an avatar for the user so that they can upload a profile photo.
+We can start by creating a new component:
+
+```dart title="lib/components/avatar.dart"
+import 'package:flutter/material.dart';
+import 'package:image_picker/image_picker.dart';
+import 'package:supabase_quickstart/utils/constants.dart';
+
+class Avatar extends StatefulWidget {
+ const Avatar({
+ Key? key,
+ required this.imageUrl,
+ required this.onUpload,
+ }) : super(key: key);
+
+ final String? imageUrl;
+ final void Function(String) onUpload;
+
+ @override
+ _AvatarState createState() => _AvatarState();
+}
+
+class _AvatarState extends State {
+ bool _isLoading = false;
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ children: [
+ if (widget.imageUrl == null)
+ Container(
+ width: 150,
+ height: 150,
+ color: Colors.grey,
+ child: const Center(
+ child: Text('No Image'),
+ ),
+ )
+ else
+ Image.network(
+ widget.imageUrl!,
+ width: 150,
+ height: 150,
+ fit: BoxFit.cover,
+ ),
+ ElevatedButton(
+ onPressed: _isLoading ? null : _upload,
+ child: const Text('Upload'),
+ ),
+ ],
+ );
+ }
+
+ Future _upload() async {
+ final _picker = ImagePicker();
+ final imageFile = await _picker.pickImage(
+ source: ImageSource.gallery,
+ maxWidth: 300,
+ maxHeight: 300,
+ );
+ if (imageFile == null) {
+ return;
+ }
+ setState(() => _isLoading = true);
+
+ final bytes = await imageFile.readAsBytes();
+ final fileExt = imageFile.path.split('.').last;
+ final fileName = '${DateTime.now().toIso8601String()}.$fileExt';
+ final filePath = fileName;
+ final response =
+ await supabase.storage.from('avatars').uploadBinary(filePath, bytes);
+
+ setState(() => _isLoading = false);
+
+ final error = response.error;
+ if (error != null) {
+ context.showErrorSnackBar(message: error.message);
+ return;
+ }
+ final imageUrlResponse =
+ supabase.storage.from('avatars').getPublicUrl(filePath);
+ widget.onUpload(imageUrlResponse.data!);
+ }
+}
+```
+
+
+### Add the new widget
+
+And then we can add the widget to the Account page as well as some logic to update the `avatar_url` whenever the user uploads a new avatar.
+
+```dart title="lib/pages/account_page.dart"
+import 'package:flutter/material.dart';
+import 'package:supabase/supabase.dart';
+import 'package:supabase_quickstart/components/auth_required_state.dart';
+import 'package:supabase_quickstart/components/avatar.dart';
+import 'package:supabase_quickstart/utils/constants.dart';
+
+class AccountPage extends StatefulWidget {
+ const AccountPage({Key? key}) : super(key: key);
+
+ @override
+ _AccountPageState createState() => _AccountPageState();
+}
+
+class _AccountPageState extends AuthRequiredState {
+ final _usernameController = TextEditingController();
+ final _websiteController = TextEditingController();
+ String? _userId;
+ String? _avatarUrl;
+ var _loading = false;
+
+ /// Called once a user id is received within `onAuthenticated()`
+ Future _getProfile(String userId) async {
+ setState(() {
+ _loading = true;
+ });
+ final response = await supabase
+ .from('profiles')
+ .select()
+ .eq('id', userId)
+ .single()
+ .execute();
+ final error = response.error;
+ if (error != null && response.status != 406) {
+ context.showErrorSnackBar(message: error.message);
+ }
+ final data = response.data;
+ if (data != null) {
+ _usernameController.text = (data['username'] ?? '') as String;
+ _websiteController.text = (data['website'] ?? '') as String;
+ _avatarUrl = (data['avatar_url'] ?? '') as String;
+ }
+ setState(() {
+ _loading = false;
+ });
+ }
+
+ /// Called when user taps `Update` button
+ Future _updateProfile() async {
+ setState(() {
+ _loading = true;
+ });
+ final userName = _usernameController.text;
+ final website = _websiteController.text;
+ final user = supabase.auth.currentUser;
+ final updates = {
+ 'id': user!.id,
+ 'username': userName,
+ 'website': website,
+ 'updated_at': DateTime.now().toIso8601String(),
+ };
+ final response = await supabase.from('profiles').upsert(updates).execute();
+ final error = response.error;
+ if (error != null) {
+ context.showErrorSnackBar(message: error.message);
+ } else {
+ context.showSnackBar(message: 'Successfully updated profile!');
+ }
+ setState(() {
+ _loading = false;
+ });
+ }
+
+ Future _signOut() async {
+ final response = await supabase.auth.signOut();
+ final error = response.error;
+ if (error != null) {
+ context.showErrorSnackBar(message: error.message);
+ }
+ Navigator.of(context).pushReplacementNamed('/login');
+ }
+
+ /// Called when image has been uploaded to Supabase storage from within Avatar widget
+ Future _onUpload(String imageUrl) async {
+ final response = await supabase.from('profiles').upsert({
+ 'id': _userId,
+ 'avatar_url': imageUrl,
+ }).execute();
+ final error = response.error;
+ if (error != null) {
+ context.showErrorSnackBar(message: error.message);
+ }
+ setState(() {
+ _avatarUrl = imageUrl;
+ });
+ context.showSnackBar(message: 'Updated your profile image!');
+ }
+
+ @override
+ void onAuthenticated(Session session) {
+ final user = session.user;
+ if (user != null) {
+ _userId = user.id;
+ _getProfile(user.id);
+ }
+ }
+
+ @override
+ void onUnauthenticated() {
+ Navigator.of(context).pushReplacementNamed('/login');
+ }
+
+ @override
+ void dispose() {
+ _usernameController.dispose();
+ _websiteController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: const Text('Profile')),
+ body: ListView(
+ padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 12),
+ children: [
+ Avatar(
+ imageUrl: _avatarUrl,
+ onUpload: _onUpload,
+ ),
+ const SizedBox(height: 18),
+ TextFormField(
+ controller: _usernameController,
+ decoration: const InputDecoration(labelText: 'User Name'),
+ ),
+ const SizedBox(height: 18),
+ TextFormField(
+ controller: _websiteController,
+ decoration: const InputDecoration(labelText: 'Website'),
+ ),
+ const SizedBox(height: 18),
+ ElevatedButton(
+ onPressed: _updateProfile,
+ child: Text(_loading ? 'Saving...' : 'Update')),
+ const SizedBox(height: 18),
+ ElevatedButton(onPressed: _signOut, child: const Text('Sign Out')),
+ ],
+ ),
+ );
+ }
+}
+```
+
+Congratulations, that is it! You have now built a fully functional user management app using Flutter and Supabase!
## Next steps
diff --git a/web/static/img/supabase-flutter-account-page.png b/web/static/img/supabase-flutter-account-page.png
new file mode 100644
index 00000000000..1f275727b7c
Binary files /dev/null and b/web/static/img/supabase-flutter-account-page.png differ
diff --git a/web/static/img/supabase-flutter-demo.png b/web/static/img/supabase-flutter-demo.png
index 11653b261b8..a9bde86c590 100644
Binary files a/web/static/img/supabase-flutter-demo.png and b/web/static/img/supabase-flutter-demo.png differ