Skip to content

Commit

Permalink
Merge pull request #7 from SandroMaglione/6-functional-programming
Browse files Browse the repository at this point in the history
Functional programming (`fpdart`) [6]
  • Loading branch information
SandroMaglione authored Dec 4, 2022
2 parents 18740de + 7e167a2 commit 572c38b
Show file tree
Hide file tree
Showing 16 changed files with 283 additions and 98 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ The project is organized in [releases](https://github.com/SandroMaglione/flutter
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)
6. [Functional programming (`fpdart`)](https://github.com/SandroMaglione/flutter-supabase-template/tree/v6-functional-programming)

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

Expand All @@ -69,6 +70,7 @@ You can review each set of changes individually by [**looking at each release**]
- [supabase_flutter](https://pub.dev/packages/supabase_flutter): Supabase SDK for Flutter's applications
- [auto_route](https://pub.dev/packages/auto_route): Routing and navigation using auto-generation
- [injectable](https://pub.dev/packages/injectable) ([get_it](https://pub.dev/packages/get_it)): Dependency injection using auto-generation
- [fpdart](https://pub.dev/packages/fpdart): Functional programming in dart and flutter

> **Note**: This setup is opinionated. There are many other possible solutions and packages to achieve the same (or better) result. It would be interesting to start a discussion about each solution (by [opening new PRs](https://github.com/SandroMaglione/flutter-supabase-template/pulls) implementing other options)
Expand All @@ -79,13 +81,12 @@ Each feature in the app has a [**blog post**](https://www.sandromaglione.com/) a
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)
6. [Functional programming (`fpdart`)](https://www.sandromaglione.com/techblog/flutter-dart-functional-programming-fpdart-supabase-app)

## 🛣 Roadmap
- [x] Adding support for [Supabase database](https://supabase.com/docs/guides/database)
- [x] Improving the code using [**fpdart**](https://pub.dev/packages/fpdart) (Functional Programming)
- [ ] 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
- [ ] Convert less-specific types to more specific ones using guards (e.g. instead of `String` for `userId`, create a `UserId` type and check the validity using `Either`)

## 🙏🏼 Contribution
Every feedback, feature request, and contribution is gladly accepted:
Expand Down
32 changes: 32 additions & 0 deletions lib/app/failures/get_user_information_failure.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
abstract class GetUserInformationFailure {
const GetUserInformationFailure();

String get mapToErrorMessage {
if (this is RequestGetUserInformationFailure) {
return 'Error when getting user information';
} else if (this is ResponseFormatErrorGetUserInformationFailure) {
return 'Invalid response';
} else if (this is JsonDecodeGetUserInformationFailure) {
return 'Missing valid user information';
}

return 'Unexpected error, please try again';
}
}

class RequestGetUserInformationFailure extends GetUserInformationFailure {
final Object error;
final StackTrace stackTrace;
const RequestGetUserInformationFailure(this.error, this.stackTrace);
}

class ResponseFormatErrorGetUserInformationFailure
extends GetUserInformationFailure {
final dynamic response;
const ResponseFormatErrorGetUserInformationFailure(this.response);
}

class JsonDecodeGetUserInformationFailure extends GetUserInformationFailure {
final Map<String, dynamic> data;
const JsonDecodeGetUserInformationFailure(this.data);
}
32 changes: 32 additions & 0 deletions lib/app/failures/login_failure.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
abstract class LoginFailure {
const LoginFailure();

String get mapToErrorMessage {
final failure = this;
if (failure is AuthErrorLoginFailure) {
return failure.message;
} else if (failure is ExecutionErrorLoginFailure) {
return 'Error when making login request';
} else if (failure is MissingUserIdLoginFailure) {
return 'Missing user information';
}

return 'Unexpected error, please try again';
}
}

class AuthErrorLoginFailure extends LoginFailure {
final String message;
final String? statusCode;
const AuthErrorLoginFailure(this.message, this.statusCode);
}

class ExecutionErrorLoginFailure extends LoginFailure {
final Object error;
final StackTrace stackTrace;
const ExecutionErrorLoginFailure(this.error, this.stackTrace);
}

class MissingUserIdLoginFailure extends LoginFailure {
const MissingUserIdLoginFailure();
}
17 changes: 17 additions & 0 deletions lib/app/failures/sign_out_failure.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
abstract class SignOutFailure {
const SignOutFailure();

String get mapToErrorMessage {
if (this is ExecutionErrorSignOutFailure) {
return 'Error when making sign out request';
}

return 'Unexpected error, please try again';
}
}

class ExecutionErrorSignOutFailure extends SignOutFailure {
final Object error;
final StackTrace stackTrace;
const ExecutionErrorSignOutFailure(this.error, this.stackTrace);
}
17 changes: 17 additions & 0 deletions lib/app/failures/update_user_information_failure.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
abstract class UpdateUserInformationFailure {
const UpdateUserInformationFailure();

String get mapToErrorMessage {
if (this is RequestUpdateUserInformationFailure) {
return 'Error when updating user information';
}

return 'Unexpected error, please try again';
}
}

class RequestUpdateUserInformationFailure extends UpdateUserInformationFailure {
final Object error;
final StackTrace stackTrace;
const RequestUpdateUserInformationFailure(this.error, this.stackTrace);
}
25 changes: 15 additions & 10 deletions lib/app/pages/home_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class HomePage extends StatelessWidget {
body: Column(
children: [
ElevatedButton(
onPressed: _onClickSignOut,
onPressed: () => _onClickSignOut(context),
child: const Text("Sign out"),
),
UserInformationText(userId: user.id),
Expand All @@ -30,13 +30,18 @@ class HomePage extends StatelessWidget {
);
}

Future<void> _onClickSignOut() async {
try {
await getIt<AuthRepository>().signOut();
} catch (e) {
// TODO: Show proper error to users
print("Error on sign out");
print(e);
}
}
Future<void> _onClickSignOut(BuildContext context) async =>
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
await getIt<AuthRepository>()
.signOut()
.match(
(signOutFailure) => signOutFailure.mapToErrorMessage,
(_) => "Sign out successful",
)
.run(),
),
),
);
}
23 changes: 14 additions & 9 deletions lib/app/pages/sign_in_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,20 @@ class _SignInPageState extends State<SignInPage> {
);
}

Future<void> _onClickSignIn(BuildContext context) async {
try {
await getIt<AuthRepository>().signInEmailAndPassword(email, password);
} catch (e) {
// TODO: Show proper error to users
print("Sign in error");
print(e);
}
}
Future<void> _onClickSignIn(BuildContext context) async =>
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
await getIt<AuthRepository>()
.signInEmailAndPassword(email, password)
.match(
(loginFailure) => loginFailure.mapToErrorMessage,
(_) => "Sign in successful",
)
.run(),
),
),
);

void _onClickGoToSignUp(BuildContext context) {
context.router.push(const SignUpRoute());
Expand Down
23 changes: 14 additions & 9 deletions lib/app/pages/sign_up_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,20 @@ class _SignUpPageState extends State<SignUpPage> {
);
}

Future<void> _onClickSignUp(BuildContext context) async {
try {
await getIt<AuthRepository>().signUpEmailAndPassword(email, password);
} catch (e) {
// TODO: Show proper error to users
print("Sign up error");
print(e);
}
}
Future<void> _onClickSignUp(BuildContext context) async =>
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
await getIt<AuthRepository>()
.signUpEmailAndPassword(email, password)
.match(
(loginFailure) => loginFailure.mapToErrorMessage,
(_) => "Sign up successful",
)
.run(),
),
),
);

void _onClickGoToSignIn(BuildContext context) {
context.router.pop();
Expand Down
17 changes: 14 additions & 3 deletions lib/app/repository/auth_repository.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import 'package:flutter_supabase_complete/app/failures/login_failure.dart';
import 'package:flutter_supabase_complete/app/failures/sign_out_failure.dart';
import 'package:fpdart/fpdart.dart';

abstract class AuthRepository {
Future<String> signInEmailAndPassword(String email, String password);
Future<String> signUpEmailAndPassword(String email, String password);
TaskEither<LoginFailure, String> signInEmailAndPassword(
String email,
String password,
);

TaskEither<LoginFailure, String> signUpEmailAndPassword(
String email,
String password,
);

Future<void> signOut();
TaskEither<SignOutFailure, Unit> signOut();
}
12 changes: 10 additions & 2 deletions lib/app/repository/user_database_repository.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import 'package:flutter_supabase_complete/app/failures/get_user_information_failure.dart';
import 'package:flutter_supabase_complete/app/failures/update_user_information_failure.dart';
import 'package:flutter_supabase_complete/app/models/user_model.dart';
import 'package:fpdart/fpdart.dart';

abstract class UserDatabaseRepository {
Future<UserModel> getUserInformation(String userId);
Future<UserModel> updateUserInformation(UserModel userModel);
TaskEither<GetUserInformationFailure, UserModel> getUserInformation(
String userId,
);

TaskEither<UpdateUserInformationFailure, UserModel> updateUserInformation(
UserModel userModel,
);
}
72 changes: 42 additions & 30 deletions lib/app/services/supabase_auth_repository.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import 'package:flutter_supabase_complete/app/failures/login_failure.dart';
import 'package:flutter_supabase_complete/app/failures/sign_out_failure.dart';
import 'package:flutter_supabase_complete/app/repository/auth_repository.dart';
import 'package:fpdart/fpdart.dart';
import 'package:injectable/injectable.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

Expand All @@ -8,38 +11,47 @@ class SupabaseAuthRepository implements AuthRepository {
const SupabaseAuthRepository(this._supabase);

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

final userId = response.user?.id;
if (userId == null) {
throw UnimplementedError();
}

return userId;
}
TaskEither<LoginFailure, String> signInEmailAndPassword(
String email,
String password,
) =>
_loginRequest(
() => _supabase.client.auth
.signInWithPassword(email: email, password: password),
);

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

final userId = response.user?.id;
if (userId == null) {
throw UnimplementedError();
}

return userId;
}
TaskEither<LoginFailure, String> signUpEmailAndPassword(
String email,
String password,
) =>
_loginRequest(
() => _supabase.client.auth.signUp(email: email, password: password),
);

@override
Future<void> signOut() async {
await _supabase.client.auth.signOut();
return;
}
TaskEither<SignOutFailure, Unit> signOut() => TaskEither.tryCatch(() async {
await _supabase.client.auth.signOut();
return unit;
}, ExecutionErrorSignOutFailure.new);

/// Shared logic for login requests (sign in and sign up).
TaskEither<LoginFailure, String> _loginRequest(
Future<AuthResponse> Function() request,
) =>
TaskEither<LoginFailure, AuthResponse>.tryCatch(
request,
(error, stackTrace) {
if (error is AuthException) {
return AuthErrorLoginFailure(error.message, error.statusCode);
}

return ExecutionErrorLoginFailure(error, stackTrace);
},
).map((response) => response.user?.id).flatMap(
(id) => Either.fromNullable(
id,
(_) => const MissingUserIdLoginFailure(),
).toTaskEither(),
);
}
Loading

0 comments on commit 572c38b

Please sign in to comment.