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
📦 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,
});
}
🔹 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);
}
}
🔹 3. Repository Interface
// domain/repositories/ride_repository.dart
import '../entities/ride.dart';
abstract class RideRepository {
Future<Ride> requestRide(String pickup, String destination);
}
🔹 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'],
);
}
}
// 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);
}
}
🔹 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);
}
}
🔹 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);
}
// 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()));
}
}
}
🔹 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'),
),
);
},
),
);
}
}
🧪 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
orshared_preferences
- Write unit and widget tests
Top comments (0)