tutorial Video

Clean Architecture in Flutter: La Guida Completa

Scopri come implementare la Clean Architecture nelle tue app Flutter per creare codice maintanibile, testabile e scalabile. Una guida pratica con esempi reali.

Stefano Baiardi di Stefano Baiardi
15 gennaio 2024
Guarda il Video
#flutter #clean-architecture #design-patterns #mobile-development

Introduzione alla Clean Architecture

Nel video di oggi esploriamo uno dei pattern architetturali più importanti per lo sviluppo mobile: la Clean Architecture. Questo approccio, ideato da Robert C. Martin (Uncle Bob), ci permette di creare applicazioni più mantenibili, testabili e indipendenti dai framework.

Perché Clean Architecture?

La Clean Architecture risolve molti problemi comuni nello sviluppo mobile:

  • Separazione delle responsabilità: Ogni layer ha un compito specifico
  • Testabilità: Il codice business è completamente isolato
  • Indipendenza dal framework: Il core della logica non dipende da Flutter
  • Scalabilità: Facilità nell’aggiungere nuove funzionalità

I Layer della Clean Architecture

1. Presentation Layer

Il layer di presentazione contiene tutto ciò che riguarda l’interfaccia utente:

class UserProfileBloc extends Bloc<UserProfileEvent, UserProfileState> {
  final GetUserUseCase getUserUseCase;
  
  UserProfileBloc({required this.getUserUseCase}) : super(UserProfileInitial()) {
    on<LoadUserProfile>(_onLoadUserProfile);
  }
  
  Future<void> _onLoadUserProfile(
    LoadUserProfile event,
    Emitter<UserProfileState> emit,
  ) async {
    emit(UserProfileLoading());
    
    final result = await getUserUseCase(UserParams(id: event.userId));
    
    result.fold(
      (failure) => emit(UserProfileError(failure.message)),
      (user) => emit(UserProfileLoaded(user)),
    );
  }
}

2. Domain Layer

Il cuore della nostra applicazione, completamente indipendente:

abstract class UserRepository {
  Future<Either<Failure, User>> getUser(String id);
  Future<Either<Failure, List<User>>> getUsers();
}

class GetUserUseCase {
  final UserRepository repository;
  
  GetUserUseCase(this.repository);
  
  Future<Either<Failure, User>> call(UserParams params) {
    return repository.getUser(params.id);
  }
}

3. Data Layer

Gestisce l’accesso ai dati esterni:

class UserRepositoryImpl implements UserRepository {
  final UserRemoteDataSource remoteDataSource;
  final UserLocalDataSource localDataSource;
  final NetworkInfo networkInfo;
  
  UserRepositoryImpl({
    required this.remoteDataSource,
    required this.localDataSource,
    required this.networkInfo,
  });
  
  @override
  Future<Either<Failure, User>> getUser(String id) async {
    if (await networkInfo.isConnected) {
      try {
        final user = await remoteDataSource.getUser(id);
        await localDataSource.cacheUser(user);
        return Right(user.toDomain());
      } catch (e) {
        return Left(ServerFailure());
      }
    } else {
      try {
        final user = await localDataSource.getUser(id);
        return Right(user.toDomain());
      } catch (e) {
        return Left(CacheFailure());
      }
    }
  }
}

Vantaggi Pratici

Testing Semplificato

Con questa architettura, testare diventa incredibilmente semplice:

void main() {
  group('GetUserUseCase', () {
    late GetUserUseCase useCase;
    late MockUserRepository mockRepository;
    
    setUp(() {
      mockRepository = MockUserRepository();
      useCase = GetUserUseCase(mockRepository);
    });
    
    test('should return user when repository call is successful', () async {
      // arrange
      const testUser = User(id: '1', name: 'Test User');
      when(() => mockRepository.getUser(any()))
          .thenAnswer((_) async => const Right(testUser));
      
      // act
      final result = await useCase(const UserParams(id: '1'));
      
      // assert
      expect(result, const Right(testUser));
    });
  });
}

Dependency Injection

Utilizziamo get_it per gestire le dipendenze:

final sl = GetIt.instance;

Future<void> init() async {
  // Use cases
  sl.registerLazySingleton(() => GetUserUseCase(sl()));
  
  // Repository
  sl.registerLazySingleton<UserRepository>(
    () => UserRepositoryImpl(
      remoteDataSource: sl(),
      localDataSource: sl(),
      networkInfo: sl(),
    ),
  );
  
  // Data sources
  sl.registerLazySingleton<UserRemoteDataSource>(
    () => UserRemoteDataSourceImpl(client: sl()),
  );
}

Conclusioni

La Clean Architecture non è solo un pattern teorico, ma uno strumento pratico che migliora drasticamente la qualità del codice. Nel video approfondiamo ogni aspetto con esempi pratici e casi d’uso reali.

Prossimi Passi

  1. Guarda il video completo per vedere l’implementazione in azione
  2. Scarica il codice esempio dal repository
  3. Prova ad applicare questi concetti al tuo prossimo progetto Flutter

Hai domande o dubbi? Lascia un commento nel video YouTube o contattami sui social!