Skip to content

Commit

Permalink
Merge pull request #6 from SandroMaglione/database
Browse files Browse the repository at this point in the history
Supabase database [5]
  • Loading branch information
SandroMaglione authored Nov 29, 2022
2 parents 141aa3e + 820c2b3 commit 18740de
Show file tree
Hide file tree
Showing 15 changed files with 263 additions and 33 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ The project is organized in [releases](https://github.com/SandroMaglione/flutter
2. [Routing (`auto_route`)](https://github.com/SandroMaglione/flutter-supabase-template/tree/v2-navigation)
3. [Dependency injection (`injectable`)](https://github.com/SandroMaglione/flutter-supabase-template/tree/v3-dep-injection)
4. [Supabase authentication](https://github.com/SandroMaglione/flutter-supabase-template/tree/v4-supabase-auth)
5. [Supabase database](https://github.com/SandroMaglione/flutter-supabase-template/tree/v5-supabase-database)

You can review each set of changes individually by [**looking at each release**](https://github.com/SandroMaglione/flutter-supabase-template/tags).

Expand All @@ -77,9 +78,10 @@ Each feature in the app has a [**blog post**](https://www.sandromaglione.com/) a
2. [Routing (`auto_route`)](https://www.sandromaglione.com/techblog/how-to-setup-routing-flutter-app)
3. [Dependency injection (`injectable`)](https://www.sandromaglione.com/techblog/how_to_implement_dependecy_injection_in_flutter)
4. [Supabase authentication](https://www.sandromaglione.com/techblog/flutter-supabase-authentication-complete-tutorial)
5. [Supabase database](https://www.sandromaglione.com/techblog/flutter-supabase-database-complete-tutorial)

## 🛣 Roadmap
- [ ] Adding support for [Supabase database](https://supabase.com/docs/guides/database)
- [x] Adding support for [Supabase database](https://supabase.com/docs/guides/database)
- [ ] Adding support for [Supabase storage](https://supabase.com/docs/guides/storage)
- [ ] Improving the code using [**fpdart**](https://pub.dev/packages/fpdart) (Functional Programming)
- [ ] Using `TaskEither` in [`repository`](lib/app/repository) as return type
Expand Down
16 changes: 12 additions & 4 deletions lib/app.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_supabase_complete/app/repository/auth_repository.dart';
import 'package:flutter_supabase_complete/core/routes/app_router.dart';
import 'package:flutter_supabase_complete/injectable.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

/// Entry widget of the app.
Expand All @@ -19,11 +21,17 @@ class _AppState extends State<App> {

/// Listen for authentication events and redirect to
/// correct page when key events are detected.
SupabaseAuth.instance.onAuthChange.listen((event) {
Supabase.instance.client.auth.onAuthStateChange.listen((authState) {
final event = authState.event;
final session = authState.session;
if (event == AuthChangeEvent.signedIn) {
_appRouter
..popUntilRoot()
..replace(const HomeRoute());
if (session != null) {
_appRouter
..popUntilRoot()
..replace(HomeRoute(user: session.user));
} else {
getIt<AuthRepository>().signOut();
}
} else if (event == AuthChangeEvent.signedOut) {
_appRouter
..popUntilRoot()
Expand Down
27 changes: 27 additions & 0 deletions lib/app/models/user_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/// Convert database model for `user` table to
/// internal dart `class`:
/// - Use `fromJson` method to convert supabase response to [UserModel]
/// - Use `toJson` method to convert [UserModel] for update request
class UserModel {
final String id;
final String? firstName;
final String? lastName;

const UserModel({
required this.id,
this.firstName,
this.lastName,
});

static UserModel fromJson(Map<String, dynamic> json) => UserModel(
id: json['id'] as String,
firstName: json['first_name'] as String?,
lastName: json['last_name'] as String?,
);

Map<String, dynamic> toJson() => <String, dynamic>{
'id': id,
'first_name': firstName,
'last_name': lastName,
};
}
15 changes: 12 additions & 3 deletions lib/app/pages/home_page.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import 'package:flutter/material.dart';
import 'package:flutter_supabase_complete/app/repository/auth_repository.dart';
import 'package:flutter_supabase_complete/app/widgets/update_user_form.dart';
import 'package:flutter_supabase_complete/app/widgets/user_information_text.dart';
import 'package:flutter_supabase_complete/injectable.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
final User user;
const HomePage({
required this.user,
Key? key,
}) : super(key: key);

@override
Widget build(BuildContext context) {
Expand All @@ -12,16 +19,18 @@ class HomePage extends StatelessWidget {
body: Column(
children: [
ElevatedButton(
onPressed: () => _onClickSignOut(context),
onPressed: _onClickSignOut,
child: const Text("Sign out"),
),
UserInformationText(userId: user.id),
UpdateUserForm(userId: user.id),
],
),
),
);
}

Future<void> _onClickSignOut(BuildContext context) async {
Future<void> _onClickSignOut() async {
try {
await getIt<AuthRepository>().signOut();
} catch (e) {
Expand Down
12 changes: 9 additions & 3 deletions lib/app/pages/splash_screen_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,15 @@ class _SplashScreenPageState extends State<SplashScreenPage> {
final session = responseList.first as Session?;

/// Redirect to either home or sign in routes based on current session.
context.router.replace(
session != null ? const HomeRoute() : const SignInRoute(),
);
if (session != null) {
context.router.replace(
HomeRoute(user: session.user),
);
} else {
context.router.replace(
const SignInRoute(),
);
}
}).catchError((_) {
context.router.replace(const SignInRoute());
});
Expand Down
6 changes: 6 additions & 0 deletions lib/app/repository/user_database_repository.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import 'package:flutter_supabase_complete/app/models/user_model.dart';

abstract class UserDatabaseRepository {
Future<UserModel> getUserInformation(String userId);
Future<UserModel> updateUserInformation(UserModel userModel);
}
7 changes: 5 additions & 2 deletions lib/app/services/supabase_auth_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class SupabaseAuthRepository implements AuthRepository {

@override
Future<String> signInEmailAndPassword(String email, String password) async {
final response = await _supabase.client.auth.signIn(
final response = await _supabase.client.auth.signInWithPassword(
email: email,
password: password,
);
Expand All @@ -24,7 +24,10 @@ class SupabaseAuthRepository implements AuthRepository {

@override
Future<String> signUpEmailAndPassword(String email, String password) async {
final response = await _supabase.client.auth.signUp(email, password);
final response = await _supabase.client.auth.signUp(
email: email,
password: password,
);

final userId = response.user?.id;
if (userId == null) {
Expand Down
32 changes: 32 additions & 0 deletions lib/app/services/supabase_user_database_repository.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import 'package:flutter_supabase_complete/app/models/user_model.dart';
import 'package:flutter_supabase_complete/app/repository/user_database_repository.dart';
import 'package:flutter_supabase_complete/core/config/supabase_table.dart';
import 'package:injectable/injectable.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

@Injectable(as: UserDatabaseRepository)
class SupabaseDatabaseRepository implements UserDatabaseRepository {
final Supabase _supabase;
final UserSupabaseTable _userSupabaseTable;
const SupabaseDatabaseRepository(this._supabase, this._userSupabaseTable);

@override
Future<UserModel> getUserInformation(String userId) async {
final response = await _supabase.client
.from(_userSupabaseTable.tableName)
.select()
.eq(_userSupabaseTable.idColumn, userId)
.single();

final userModel = UserModel.fromJson(response as Map<String, dynamic>);
return userModel;
}

@override
Future<UserModel> updateUserInformation(UserModel userModel) async {
await _supabase.client
.from(_userSupabaseTable.tableName)
.update(userModel.toJson());
return userModel;
}
}
58 changes: 58 additions & 0 deletions lib/app/widgets/update_user_form.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
import 'package:flutter_supabase_complete/app/models/user_model.dart';
import 'package:flutter_supabase_complete/app/repository/user_database_repository.dart';
import 'package:flutter_supabase_complete/injectable.dart';

class UpdateUserForm extends StatefulWidget {
final String userId;
const UpdateUserForm({
required this.userId,
Key? key,
}) : super(key: key);

@override
State<UpdateUserForm> createState() => _UpdateUserFormState();
}

class _UpdateUserFormState extends State<UpdateUserForm> {
String firstName = "";
String lastName = "";

@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
onChanged: (value) => setState(() {
firstName = value;
}),
),
TextField(
onChanged: (value) => setState(() {
lastName = value;
}),
),
ElevatedButton(
onPressed: _onClickUpdateUser,
child: const Text("Update"),
),
],
);
}

Future<void> _onClickUpdateUser() async {
try {
await getIt<UserDatabaseRepository>().updateUserInformation(
UserModel(
id: widget.userId,
firstName: firstName,
lastName: lastName,
),
);
} catch (e) {
// TODO: Show proper error to users
print("Error when updating user information");
print(e);
}
}
}
33 changes: 33 additions & 0 deletions lib/app/widgets/user_information_text.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import 'package:flutter_supabase_complete/app/models/user_model.dart';
import 'package:flutter_supabase_complete/app/repository/user_database_repository.dart';
import 'package:flutter_supabase_complete/injectable.dart';

class UserInformationText extends StatelessWidget {
final String userId;
const UserInformationText({
required this.userId,
Key? key,
}) : super(key: key);

@override
Widget build(BuildContext context) {
return FutureBuilder<UserModel>(
future: getIt<UserDatabaseRepository>().getUserInformation(userId),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
} else if (snapshot.connectionState == ConnectionState.done) {
final data = snapshot.data;
if (data != null) {
return Text(data.firstName ?? "No name");
}

return const Text("No found");
}

return const Text("Error");
},
);
}
}
21 changes: 21 additions & 0 deletions lib/core/config/supabase_table.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import 'package:injectable/injectable.dart';

/// Set of all the database tables in Supabase.
///
/// Used to reference valid tables when making database requests.
abstract class SupabaseTable {
const SupabaseTable();
String get tableName;
}

@lazySingleton
class UserSupabaseTable implements SupabaseTable {
const UserSupabaseTable();

@override
String get tableName => "user";

String get idColumn => "id";
String get idFirstName => "first_name";
String get idLastName => "last_name";
}
3 changes: 3 additions & 0 deletions lib/core/routes/app_router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import 'package:flutter_supabase_complete/app/pages/sign_in_page.dart';
import 'package:flutter_supabase_complete/app/pages/sign_up_page.dart';
import 'package:flutter_supabase_complete/app/pages/splash_screen_page.dart';

/// Make sure to import `supabase_flutter` to provide its classes to `auto_route`
import 'package:supabase_flutter/supabase_flutter.dart';

part 'app_router.gr.dart';

@MaterialAutoRouter(
Expand Down
15 changes: 15 additions & 0 deletions lib/main_common.dart
Original file line number Diff line number Diff line change
@@ -1,15 +1,30 @@
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter_supabase_complete/app.dart';
import 'package:flutter_supabase_complete/constants.dart';
import 'package:flutter_supabase_complete/injectable.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

/// Used to validate certificates for local development.
class MyHttpOverrides extends HttpOverrides {
@override
HttpClient createHttpClient(SecurityContext? context) {
return super.createHttpClient(context)
..badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
}
}

/// Shared `runApp` configuration.
///
/// Used to initialize all required dependencies, packages, and constants.
Future<void> mainCommon() async {
WidgetsFlutterBinding.ensureInitialized();

/// Used to validate certificates for local development
HttpOverrides.global = MyHttpOverrides();

// Dependency injection (injectable)
configureDependencies();

Expand Down
Loading

0 comments on commit 18740de

Please sign in to comment.