DEV Community

Cover image for Building a Scalable Folder Structure in Flutter Using Clean Architecture + BLoC/Cubit
Md. Al-Amin
Md. Al-Amin

Posted on

Building a Scalable Folder Structure in Flutter Using Clean Architecture + BLoC/Cubit

You’ve probably started a Flutter project that worked great—until it didn’t. As the app grows, you find files everywhere, business logic mixed with UI, and no way to test or reuse code easily. This problem scales fast in real-world apps like Uber, Pathao, or any ridesharing, delivery, or e-commerce app.

So how do successful teams handle it?

They use a scalable architecture—most commonly, Clean Architecture—combined with Cubit/BLoC for state management.

In this blog, we’ll:

  • Understand Clean Architecture in Flutter
  • Build a real-world feature (e.g., ride booking like Uber/Pathao)
  • Create a folder structure that scales with your app
  • Use Cubit for state management in a clean way

🧠 What is Clean Architecture?

Clean Architecture, introduced by Robert C. Martin (Uncle Bob), separates code into layers:

  • Presentation Layer (UI + State Management)
  • Domain Layer (Business Logic)
  • Data Layer (API, Local DB)

✅ Benefits:

  • Easy to test
  • Decoupled, maintainable code
  • Highly scalable for large teams

🧳 Real-Life Case: Ride Booking Feature (like Uber/Pathao)

Let’s say we’re building a ride booking feature:

  • User opens the app and sees available rides.
  • They choose a pickup & drop location.
  • A ride is requested and matched to a driver.

🗂 Suggested Folder Structure

Here’s a Clean Architecture folder structure that scales:

lib/
├── core/                   # Common utilities, themes, errors, constants
├── features/
│   └── ride_booking/
│       ├── data/
│       │   ├── datasources/
│       │   │   └── ride_remote_data_source.dart
│       │   ├── models/
│       │   │   └── ride_model.dart
│       │   └── repositories/
│       │       └── ride_repository_impl.dart
│       ├── domain/
│       │   ├── entities/
│       │   │   └── ride.dart
│       │   ├── repositories/
│       │   │   └── ride_repository.dart
│       │   └── usecases/
│       │       └── request_ride.dart
│       └── presentation/
│           ├── cubit/
│           │   ├── ride_cubit.dart
│           │   └── ride_state.dart
│           └── pages/
│               └── ride_booking_page.dart
├── main.dart
Enter fullscreen mode Exit fullscreen mode

📦 Layer-by-Layer Breakdown (with code)


🔹 1. Entity (Core domain object)

// domain/entities/ride.dart
class Ride {
  final String id;
  final String driverName;
  final String pickup;
  final String destination;

  Ride({
    required this.id,
    required this.driverName,
    required this.pickup,
    required this.destination,
  });
}
Enter fullscreen mode Exit fullscreen mode

🔹 2. Use Case (Business logic)

// domain/usecases/request_ride.dart
import '../entities/ride.dart';
import '../repositories/ride_repository.dart';

class RequestRide {
  final RideRepository repository;

  RequestRide(this.repository);

  Future<Ride> call(String pickup, String destination) {
    return repository.requestRide(pickup, destination);
  }
}
Enter fullscreen mode Exit fullscreen mode

🔹 3. Repository Interface

// domain/repositories/ride_repository.dart
import '../entities/ride.dart';

abstract class RideRepository {
  Future<Ride> requestRide(String pickup, String destination);
}
Enter fullscreen mode Exit fullscreen mode

🔹 4. Model & Data Source

// data/models/ride_model.dart
import '../../domain/entities/ride.dart';

class RideModel extends Ride {
  RideModel({
    required super.id,
    required super.driverName,
    required super.pickup,
    required super.destination,
  });

  factory RideModel.fromJson(Map<String, dynamic> json) {
    return RideModel(
      id: json['id'],
      driverName: json['driver_name'],
      pickup: json['pickup'],
      destination: json['destination'],
    );
  }
}
Enter fullscreen mode Exit fullscreen mode
// data/datasources/ride_remote_data_source.dart
import 'package:dio/dio.dart';
import '../models/ride_model.dart';

class RideRemoteDataSource {
  final Dio dio;

  RideRemoteDataSource(this.dio);

  Future<RideModel> requestRide(String pickup, String destination) async {
    final response = await dio.post('/ride/request', data: {
      'pickup': pickup,
      'destination': destination,
    });

    return RideModel.fromJson(response.data);
  }
}
Enter fullscreen mode Exit fullscreen mode

🔹 5. Repository Implementation

// data/repositories/ride_repository_impl.dart
import '../../domain/entities/ride.dart';
import '../../domain/repositories/ride_repository.dart';
import '../datasources/ride_remote_data_source.dart';

class RideRepositoryImpl implements RideRepository {
  final RideRemoteDataSource remote;

  RideRepositoryImpl(this.remote);

  @override
  Future<Ride> requestRide(String pickup, String destination) {
    return remote.requestRide(pickup, destination);
  }
}
Enter fullscreen mode Exit fullscreen mode

🔹 6. Cubit & State

// presentation/cubit/ride_state.dart
import '../../../domain/entities/ride.dart';

abstract class RideState {}

class RideInitial extends RideState {}

class RideLoading extends RideState {}

class RideSuccess extends RideState {
  final Ride ride;
  RideSuccess(this.ride);
}

class RideError extends RideState {
  final String message;
  RideError(this.message);
}
Enter fullscreen mode Exit fullscreen mode
// presentation/cubit/ride_cubit.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../domain/usecases/request_ride.dart';
import 'ride_state.dart';

class RideCubit extends Cubit<RideState> {
  final RequestRide requestRide;

  RideCubit(this.requestRide) : super(RideInitial());

  void bookRide(String pickup, String destination) async {
    emit(RideLoading());
    try {
      final ride = await requestRide(pickup, destination);
      emit(RideSuccess(ride));
    } catch (e) {
      emit(RideError(e.toString()));
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

🔹 7. Presentation (Page)

// presentation/pages/ride_booking_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../cubit/ride_cubit.dart';
import '../cubit/ride_state.dart';

class RideBookingPage extends StatelessWidget {
  const RideBookingPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Book a Ride')),
      body: BlocBuilder<RideCubit, RideState>(
        builder: (context, state) {
          if (state is RideLoading) {
            return const Center(child: CircularProgressIndicator());
          } else if (state is RideSuccess) {
            return Center(
                child: Text('Driver: ${state.ride.driverName} is on the way!'));
          } else if (state is RideError) {
            return Center(child: Text('Error: ${state.message}'));
          }
          return Center(
            child: ElevatedButton(
              onPressed: () => context
                  .read<RideCubit>()
                  .bookRide('Banani', 'Gulshan 2'),
              child: const Text('Request Ride'),
            ),
          );
        },
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

🧪 How This Helps in Real World (Like Uber or Pathao)

  • Scales with features: Each feature is isolated in its own module.
  • Team-friendly: Backend, frontend, QA can work without stepping on each other’s toes.
  • Easy refactoring: Want to replace Dio with Chopper later? Just update the Data Layer.
  • Testable: You can unit test the domain layer without worrying about the UI or network.

🏁 Final Thoughts

Your app will grow. Fast. And if you’re not careful, your code will turn into spaghetti.

Using Clean Architecture + Cubit is not just a fancy pattern—it's a necessity in real-world, team-driven, scalable apps like Uber or Pathao. Start small, refactor gradually, and you'll thank yourself later.


📘 What's Next?

  • Add dependency injection using get_it
  • Add offline caching with hive or shared_preferences
  • Write unit and widget tests

Top comments (0)