Flutter  

Building A Habit Tracker App In Flutter: Part 2 - Authentication & Backend Integration

In Part 1, we discussed how to set up a solid foundation using Clean Architecture. Now, in Part 2, we will focus on the backend integration and user authentication. For this project, we are using Supabase, which is an excellent open-source alternative to Firebase.

In any production application, managing user accounts securely is a very important requirement. We need to ensure that each user can only access their own data.

1. Why Supabase for our Backend?

Unlike typical NoSQL databases, Supabase gives us a full PostgreSQL database. This is very useful for a habit tracker because our data is relational—one user has many habits, and each habit has many completion records. Supabase also handles the authentication and session management automatically.

2. Setting Up the Supabase Client

First, we need to initialize the Supabase client in our main.dartfile. It is best practice to keep the configuration details in a separate file.

// main.dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Supabase.initialize(
    url: Config.supabaseUrl,
    anonKey: Config.supabaseAnonKey,
  );

  await di.init();
  runApp(const MyApp());
}

We also register the SupabaseClient in our service locator so it can be used across the application without manual creation.

3. The Authentication Repository

Following the Clean Architecture principles, we first define an interface for our authentication operations. This is basically a contract that defines what our app can do.

// domain/repositories/auth_repository.dart
abstract class AuthRepository {
  Future<Either<Failure, UserEntity>> signIn(String email, String password);
  Future<Either<Failure, UserEntity>> signUp(String email, String password);
  Future<Either<Failure, void>> signOut();
  Future<Either<Failure, UserEntity>> getCurrentUser();
}

By using the Future<Either<Failure, Success>> pattern, we handle errors gracefully. This is much better than using try-catch blocks everywhere in our UI.

4. Implementation with Supabase

The actual implementation happens in the data layer. Here, we call the Supabase Auth APIs.

// data/repositories/auth_repository_impl.dart
@override
Future<Either<Failure, UserEntity>> signIn(String email, String password) async {
  try {
    final response = await supabaseClient.auth.signInWithPassword(
      email: email,
      password: password,
    );
    if (response.user != null) {
      return Right(UserModel.fromSupabase(response.user!));
    }
    return const Left(ServerFailure('Authentication failed'));
  } on AuthException catch (e) {
    return Left(ServerFailure(e.message));
  } catch (e) {
    return Left(ServerFailure(e.toString()));
  }
}

5. State Management with BLoC

We use AuthBloc to manage the different states of authentication, such asAuthLoading,Authenticated, andUnauthenticated When the user clicks the "Sign In" button, we add an event to the BLoC:

Future<void> _onAuthSignInRequested(
  AuthSignInRequested event,
  Emitter<AuthState> emit,
) async {
  emit(AuthLoading());
  final result = await signIn(AuthParams(email: event.email, password: event.password));
  result.fold(
    (failure) => emit(AuthError(failure.message)),
    (user) => emit(Authenticated(user)),
  );
}

Conclusion

We now have a working authentication system that securely manages our users. Supabase handles the session persistence, so the user doesn't have to log in every time they open the app.

Next, we will build the core feature of our application: Creating and Managing Habits. We will discuss how to perform CRUD operations efficiently. See you in Part 3!