Code Examples

This document provides practical code examples showing how to use the key features of the Flutter Riverpod Clean Architecture template.

Using Extensions

DateTime Extensions

import 'package:flutter_riverpod_clean_architecture/core/utils/extensions/datetime_extensions.dart';

void exampleDateTimeExtensions() {
  final now = DateTime.now();

  // Format the date
  print(now.formatAs('MMMM d, yyyy')); // June 15, 2025

  // Get relative time
  print(now.subtract(Duration(minutes: 5)).timeAgo); // 5 minutes ago

  // Add time
  final tomorrow = now.addDays(1);

  // Check if date is today/tomorrow/yesterday
  print(now.isToday); // true
  print(tomorrow.isTomorrow); // true

  // Start/end of period
  final startOfMonth = now.startOfMonth;
  final endOfDay = now.endOfDay;

  // Custom week
  final weekStart = now.startOfWeek(firstDayOfWeek: DateTime.monday);
}

BuildContext Extensions

import 'package:flutter_riverpod_clean_architecture/core/utils/extensions/context_extensions.dart';

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Screen properties
    final width = context.screenWidth;
    final height = context.screenHeight;
    final isTablet = context.isTablet;
    final isDarkMode = context.isDarkMode;

    // Theme shortcuts
    final primaryColor = context.colorScheme.primary;
    final bodyTextStyle = context.textTheme.bodyMedium;

    // Localization
    final welcomeMessage = context.tr('welcome_message');
    final formattedDate = context.formatDate(DateTime.now(), 'short');
    final formattedCurrency = context.formatCurrency(19.99);

    // Navigation
    context.pop();
    context.pushNamed('/details', params: {'id': '123'});

    // UI helpers
    context.showSnackBar('Operation successful');

    return Container();
  }
}

Feature Implementation

Authentication Feature

Domain Layer (entities)

// lib/features/auth/domain/entities/user_entity.dart
class UserEntity {
  final String id;
  final String email;
  final String name;
  final String? photoUrl;

  UserEntity({
    required this.id,
    required this.email,
    required this.name,
    this.photoUrl,
  });
}

Domain Layer (repositories)

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

Domain Layer (usecases)

// lib/features/auth/domain/usecases/sign_in_usecase.dart
class SignInUseCase {
  final AuthRepository repository;

  SignInUseCase(this.repository);

  Future<Either<Failure, UserEntity>> call(SignInParams params) {
    return repository.signIn(params.email, params.password);
  }
}

class SignInParams {
  final String email;
  final String password;

  SignInParams({required this.email, required this.password});
}

Data Layer (models)

// lib/features/auth/data/models/user_model.dart
class UserModel {
  final String id;
  final String email;
  final String name;
  final String? photoUrl;

  UserModel({
    required this.id,
    required this.email,
    required this.name,
    this.photoUrl,
  });

  factory UserModel.fromJson(Map<String, dynamic> json) {
    return UserModel(
      id: json['id'],
      email: json['email'],
      name: json['name'],
      photoUrl: json['photoUrl'],
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'email': email,
      'name': name,
      'photoUrl': photoUrl,
    };
  }

  UserEntity toEntity() {
    return UserEntity(
      id: id,
      email: email,
      name: name,
      photoUrl: photoUrl,
    );
  }
}

Data Layer (datasources)

// lib/features/auth/data/datasources/auth_remote_datasource.dart
abstract class AuthRemoteDataSource {
  Future<UserModel> signIn(String email, String password);
  Future<void> signOut();
  Future<UserModel?> getCurrentUser();
}

class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
  final ApiClient apiClient;

  AuthRemoteDataSourceImpl(this.apiClient);

  @override
  Future<UserModel> signIn(String email, String password) async {
    final response = await apiClient.post('/auth/login', {
      'email': email,
      'password': password,
    });

    return UserModel.fromJson(response.data['user']);
  }

  @override
  Future<void> signOut() async {
    await apiClient.post('/auth/logout', {});
  }

  @override
  Future<UserModel?> getCurrentUser() async {
    try {
      final response = await apiClient.get('/auth/user');
      return UserModel.fromJson(response.data['user']);
    } catch (e) {
      return null;
    }
  }
}

Data Layer (repositories)

// lib/features/auth/data/repositories/auth_repository_impl.dart
class AuthRepositoryImpl implements AuthRepository {
  final AuthRemoteDataSource remoteDataSource;
  final AuthLocalDataSource localDataSource;

  AuthRepositoryImpl(this.remoteDataSource, this.localDataSource);

  @override
  Future<Either<Failure, UserEntity>> signIn(String email, String password) async {
    try {
      final userModel = await remoteDataSource.signIn(email, password);
      await localDataSource.saveUser(userModel);
      return Right(userModel.toEntity());
    } catch (e) {
      return Left(ServerFailure(message: 'Failed to sign in: ${e.toString()}'));
    }
  }

  @override
  Future<Either<Failure, void>> signOut() async {
    try {
      await remoteDataSource.signOut();
      await localDataSource.clearUser();
      return const Right(null);
    } catch (e) {
      return Left(ServerFailure(message: 'Failed to sign out: ${e.toString()}'));
    }
  }

  @override
  Future<Either<Failure, UserEntity>> getCurrentUser() async {
    try {
      // Try to get user from local storage first
      final localUser = await localDataSource.getUser();
      if (localUser != null) {
        return Right(localUser.toEntity());
      }

      // If not available locally, try to get from remote
      final remoteUser = await remoteDataSource.getCurrentUser();
      if (remoteUser != null) {
        await localDataSource.saveUser(remoteUser);
        return Right(remoteUser.toEntity());
      }

      return Left(AuthFailure(message: 'User not authenticated'));
    } catch (e) {
      return Left(ServerFailure(message: 'Failed to get current user: ${e.toString()}'));
    }
  }
}

Presentation Layer (providers)

// lib/features/auth/presentation/providers/auth_provider.dart
final authRepositoryProvider = Provider<AuthRepository>((ref) {
  final remoteDataSource = ref.watch(authRemoteDataSourceProvider);
  final localDataSource = ref.watch(authLocalDataSourceProvider);
  return AuthRepositoryImpl(remoteDataSource, localDataSource);
});

final signInUseCaseProvider = Provider<SignInUseCase>((ref) {
  final repository = ref.watch(authRepositoryProvider);
  return SignInUseCase(repository);
});

final authStateProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
  final signInUseCase = ref.watch(signInUseCaseProvider);
  final signOutUseCase = ref.watch(signOutUseCaseProvider);
  final getCurrentUserUseCase = ref.watch(getCurrentUserUseCaseProvider);

  return AuthNotifier(
    signInUseCase: signInUseCase,
    signOutUseCase: signOutUseCase,
    getCurrentUserUseCase: getCurrentUserUseCase,
  );
});

class AuthNotifier extends StateNotifier<AuthState> {
  final SignInUseCase signInUseCase;
  final SignOutUseCase signOutUseCase;
  final GetCurrentUserUseCase getCurrentUserUseCase;

  AuthNotifier({
    required this.signInUseCase,
    required this.signOutUseCase,
    required this.getCurrentUserUseCase,
  }) : super(const AuthState.initial()) {
    checkCurrentUser();
  }

  Future<void> checkCurrentUser() async {
    state = const AuthState.loading();

    final result = await getCurrentUserUseCase(NoParams());

    state = result.fold(
      (failure) => const AuthState.unauthenticated(),
      (user) => AuthState.authenticated(user),
    );
  }

  Future<void> signIn(String email, String password) async {
    state = const AuthState.loading();

    final params = SignInParams(email: email, password: password);
    final result = await signInUseCase(params);

    state = result.fold(
      (failure) => AuthState.error(failure.message),
      (user) => AuthState.authenticated(user),
    );
  }

  Future<void> signOut() async {
    state = const AuthState.loading();

    final result = await signOutUseCase(NoParams());

    state = result.fold(
      (failure) => AuthState.error(failure.message),
      (_) => const AuthState.unauthenticated(),
    );
  }
}

Presentation Layer (state)

// lib/features/auth/presentation/providers/auth_state.dart
@freezed
class AuthState with _$AuthState {
  const factory AuthState.initial() = _Initial;
  const factory AuthState.loading() = _Loading;
  const factory AuthState.authenticated(UserEntity user) = _Authenticated;
  const factory AuthState.unauthenticated() = _Unauthenticated;
  const factory AuthState.error(String message) = _Error;
}

Presentation Layer (screen)

// lib/features/auth/presentation/screens/login_screen.dart
class LoginScreen extends ConsumerStatefulWidget {
  const LoginScreen({Key? key}) : super(key: key);

  @override
  ConsumerState<LoginScreen> createState() => _LoginScreenState();
}

class _LoginScreenState extends ConsumerState<LoginScreen> {
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  final _formKey = GlobalKey<FormState>();

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  void _handleLogin() {
    if (_formKey.currentState?.validate() ?? false) {
      ref.read(authStateProvider.notifier).signIn(
        _emailController.text,
        _passwordController.text,
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    final authState = ref.watch(authStateProvider);

    return Scaffold(
      appBar: AppBar(
        title: Text(context.tr('login.title')),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Form(
          key: _formKey,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              TextFormField(
                controller: _emailController,
                decoration: InputDecoration(
                  labelText: context.tr('login.email_label'),
                ),
                validator: (value) {
                  if (value?.isEmpty ?? true) {
                    return context.tr('login.email_required');
                  }
                  if (!value!.contains('@')) {
                    return context.tr('login.email_invalid');
                  }
                  return null;
                },
              ),
              const SizedBox(height: 16),
              TextFormField(
                controller: _passwordController,
                decoration: InputDecoration(
                  labelText: context.tr('login.password_label'),
                ),
                obscureText: true,
                validator: (value) {
                  if (value?.isEmpty ?? true) {
                    return context.tr('login.password_required');
                  }
                  if ((value?.length ?? 0) < 6) {
                    return context.tr('login.password_too_short');
                  }
                  return null;
                },
              ),
              const SizedBox(height: 24),
              authState.maybeWhen(
                loading: () => const CircularProgressIndicator(),
                error: (message) => Text(message, style: TextStyle(color: Colors.red)),
                orElse: () => ElevatedButton(
                  onPressed: _handleLogin,
                  child: Text(context.tr('login.button')),
                ),
              ),
              if (authState.maybeWhen(
                error: (_) => true,
                orElse: () => false,
              ))
                const SizedBox(height: 16),
            ],
          ),
        ),
      ),
    );
  }
}

For more code examples, see the lib/examples directory in the project.