A Flutter application built with clean architecture principles and Firebase integration.
- Architecture: Clean Architecture
- State management: flutter_bloc
- Navigation: auto_route
- DI: get_it, injectable
- REST API: dio
- GraphQL: artemis, graphql_flutter
- Database: objectbox
- Shared Preferences: encrypted_shared_preferences
- Data class: freezed
- Lint: dart_code_metrics, flutter_lints
- CI/CD: Github Actions, Bitbucket Pipelines
- Unit Test: mocktail, bloc_test
- Paging: infinite_scroll_pagination
- Utils: rxdart, dartx, async
- Assets generator: flutter_gen_runner, flutter_launcher_icons, flutter_native_splash
This project implements Clean Architecture principles with a strict separation of concerns and unidirectional data flow, organized as a monorepo using Melos.
The project is organized as a monorepo with multiple packages:
mozome-flutter-app/ # Root monorepo
├── domain/ # Pure Dart - Core Business Logic
│ └── lib/
│ ├── core/ # Core utilities and base classes
│ │ ├── error/ # Failure handling
│ │ └── usecase/ # Base usecase definitions
│ ├── entities/ # Business objects
│ ├── repositories/ # Abstract contracts
│ └── usecases/ # Business logic units
│
├── data/ # Pure Dart - Data Layer
│ └── lib/
│ ├── core/ # Data layer utilities
│ ├── data/ # Data implementation
│ │ ├── models/ # Data transfer objects
│ │ ├── repositories/ # Repository implementations
│ │ └── datasources/ # Data providers
│ ├── di/ # Data layer DI
│ └── graphql/ # GraphQL specific code
│
├── app/ # Main Customer Flutter App
│ └── lib/
│ ├── core/ # App-specific utilities
│ ├── di/ # App dependency injection
│ └── presentation/ # UI Layer
│ ├── pages/ # Screens
│ ├── widgets/ # Reusable widgets
│ └── bloc/ # Business Logic Components
│
├── mozome_provider/ # Provider Flutter App Package
│ └── lib/
│ ├── core/ # App-specific utilities
│ ├── di/ # App dependency injection
│ └── presentation/ # UI Layer
│ ├── pages/ # Screens
│ ├── widgets/ # Reusable widgets
│ └── bloc/ # Business Logic Components
│
├── mozome_admin/ # Admin Flutter App Package
│ └── lib/
│ ├── core/ # App-specific utilities
│ ├── di/ # App dependency injection
│ └── presentation/ # UI Layer
│ ├── pages/ # Screens
│ ├── widgets/ # Reusable widgets
│ └── bloc/ # Business Logic Components
│
├── initializer/ # Project initialization and config
│ └── lib/
│ └── env/ # Environment configuration
│
├── tools/ # Development and build tools
│ ├── scripts/ # Build and automation scripts
│ └── generators/ # Code generators
│
├── shared/ # Shared Flutter code
│ └── lib/
│ ├── constants/ # Shared constants
│ ├── theme/ # Common theme definitions
│ ├── utils/ # Shared utilities
│ └── widgets/ # Common widgets
│
└── resources/ # Static resources
├── assets/ # Images, fonts, etc.
├── l10n/ # Localization files
└── config/ # Configuration files
Key Points:
-
Core Packages:
domain: Pure Dart business logicdata: Data layer implementationshared: Common Flutter code
-
Applications:
app: Main customer mobile appmozome_provider: Service provider app packagemozome_admin: Admin dashboard app package
-
Support Packages:
initializer: Project setup and configurationtools: Development utilities and scriptsresources: Static assets and configurations
Dependency Flow:
graph TD
A[app/mozome_provider/mozome_admin] --> B[data]
A --> C[domain]
A --> D[shared]
B --> C
D --> C
-
Pure Dart Package
- No Flutter dependencies
- No external package dependencies
- Framework agnostic
-
Key Components:
- Entities: Pure business objects
// domain/lib/entities/user.dart class User extends Equatable { final String id; final String email; // ... core business properties only }
- Repository Interfaces: Abstract contracts
// domain/lib/repositories/user_repository.dart abstract class IUserRepository { Future<Either<Failure, User>> getUser(String id); // ... business operations only }
- Use Cases: Single responsibility actions
// domain/lib/usecases/get_user.dart class GetUser extends UseCase<User, String> { final IUserRepository _repository; Future<Either<Failure, User>> call(String id) => _repository.getUser(id); }
- Entities: Pure business objects
-
Implementation Package
- Implements domain contracts
- Handles external data sources
- Manages data persistence
-
Key Components:
- Models: Data transfer objects
// data/lib/models/user_model.dart class UserModel { factory UserModel.fromJson(Map<String, dynamic> json) => ... factory UserModel.fromEntity(User entity) => ... User toEntity() => ... }
- Repository Implementations:
// data/lib/repositories/user_repository_impl.dart @Injectable(as: IUserRepository) class UserRepositoryImpl implements IUserRepository { final GraphQLClient _client; final UserMapper _mapper; // ... implementation with external dependencies }
- Data Sources:
// data/lib/datasources/user_remote_datasource.dart class UserRemoteDataSource { final GraphQLClient _client; Future<UserModel> getUser(String id) => _client.query(...); }
- Models: Data transfer objects
-
Flutter Applications
- Platform-specific implementation
- UI/UX implementation
- State management
-
Key Components:
- BLoC Pattern:
// apps/customer/lib/presentation/bloc/user_bloc.dart class UserBloc extends Bloc<UserEvent, UserState> { final GetUser _getUser; // ... UI state management }
- Pages & Widgets:
// apps/customer/lib/presentation/pages/user_profile.dart class UserProfilePage extends StatelessWidget { @override Widget build(BuildContext context) => BlocProvider( create: (context) => getIt<UserBloc>(), child: UserProfileView(), ); }
- BLoC Pattern:
graph TD
A[apps/*] --> B[data]
A --> C[domain]
B --> C
dependencies:
dartz: ^0.10.1
equatable: ^2.0.5
meta: ^1.9.1dependencies:
domain:
path: ../domain
graphql: ^5.2.0-beta.7
injectable: ^2.3.2dependencies:
domain:
path: ../../packages/domain
data:
path: ../../packages/data
flutter_bloc: ^8.1.3
auto_route: ^7.8.4- User Interaction Flow:
UI Action → BLoC Event → UseCase → Repository Interface → Implementation → DataSource
- Data Return Flow:
DataSource → Model → Entity → UseCase → BLoC State → UI Update
void main() {
test('GetUser use case should return User entity', () {
final useCase = GetUser(mockRepository);
final result = await useCase('user_id');
expect(result.isRight(), true);
});
}void main() {
test('UserRepositoryImpl should map API response to Entity', () {
final repository = UserRepositoryImpl(mockDataSource);
final result = await repository.getUser('user_id');
expect(result.isRight(), true);
});
}void main() {
blocTest<UserBloc, UserState>(
'emits [loading, loaded] when GetUser succeeds',
build: () => UserBloc(mockGetUser),
act: (bloc) => bloc.add(GetUserEvent('user_id')),
expect: () => [UserState.loading(), UserState.loaded(user)],
);
}- Domain Layer: Type-safe failures
abstract class Failure extends Equatable {
final String message;
const Failure(this.message);
}- Data Layer: Exception mapping
Future<Either<Failure, User>> getUser(String id) async {
try {
final model = await _dataSource.getUser(id);
return Right(model.toEntity());
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
}
}- Presentation Layer: User feedback
BlocBuilder<UserBloc, UserState>(
builder: (context, state) => state.maybeWhen(
error: (message) => ErrorView(message: message),
orElse: () => const SizedBox(),
),
)- Dart: 3.1.0
- Flutter SDK: 3.13.1
- Melos: 3.1.0
- CocoaPods: 1.12.0
-
WARN: If you already installed
melosandlefthook, you could omit this step. -
Install melos:
- Run
dart pub global activate melos 3.1.0
- Run
-
Install lefthook (optional):
- Run
gem install lefthook
- Run
-
Export paths:
- Add to
.zshrcor.bashrcfile
- Add to
export PATH="$PATH:<path to flutter>/flutter/bin"
export PATH="$PATH:<path to flutter>/flutter/bin/cache/dart-sdk/bin"
export PATH="$PATH:~/.pub-cache/bin"
export PATH="$PATH:~/.gem/gems/lefthook-0.7.7/bin"
- Save file `.zshrc`
- Run `source ~/.zshrc`
- cd to root folder of project
- Run
make gen_env - Run
make sync - Run
lefthook install(optional) - Run & Enjoy!
- Update Flutter version in:
- Update Melos version in:
Appbleed Pty Ltd
# data/build.yaml
targets:
$default:
builders:
graphql_codegen:
options:
clients:
- graphql
scalars:
DateTime:
type: DateTime
addTypename: true
generateHelpers: trueGenerated files:
// data/lib/graphql/generated/user.graphql.dart
@JsonSerializable()
class GetUser$Query {
final User? user;
GetUser$Query({this.user});
factory GetUser$Query.fromJson(Map<String, dynamic> json) =>
_$GetUser$QueryFromJson(json);
}// apps/customer/lib/di/injection.dart
@InjectableInit(
initializerName: 'init',
preferRelativeImports: true,
asExtension: true,
)
Future<void> configureDependencies() => getIt.init();
// apps/customer/lib/di/injection.config.dart
@InjectableInit(...)
GetIt init(GetIt getIt) {
getIt
..registerFactory<UserBloc>(
() => UserBloc(getIt<GetUser>())
)
..registerLazySingleton<IUserRepository>(
() => UserRepositoryImpl(getIt<UserRemoteDataSource>())
);
}// apps/customer/lib/presentation/bloc/user/user_state.freezed.dart
@freezed
class UserState with _$UserState {
const factory UserState.initial() = _Initial;
const factory UserState.loading() = _Loading;
const factory UserState.loaded(User user) = _Loaded;
const factory UserState.error(String message) = _Error;
}# domain/pubspec.yaml
name: domain
version: 1.0.0
publish_to: none
environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
dartz: ^0.10.1
equatable: ^2.0.5
injectable: ^2.3.2
meta: ^1.9.1
dev_dependencies:
build_runner: ^2.4.7
injectable_generator: ^2.4.1
mockito: ^5.4.3
test: ^1.24.0# data/pubspec.yaml
name: data
version: 1.0.0
publish_to: none
environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
domain:
path: ../domain
graphql: ^5.2.0-beta.7
injectable: ^2.3.2
json_annotation: ^4.8.1
dev_dependencies:
build_runner: ^2.4.7
graphql_codegen: ^0.13.11
injectable_generator: ^2.4.1
json_serializable: ^6.7.1
mockito: ^5.4.3
test: ^1.24.0# apps/customer/pubspec.yaml
name: customer
version: 1.0.0
publish_to: none
environment:
sdk: '>=3.0.0 <4.0.0'
flutter: '>=3.13.0'
dependencies:
domain:
path: ../../packages/domain
data:
path: ../../packages/data
flutter_bloc: ^8.1.3
freezed_annotation: ^2.4.1
injectable: ^2.3.2
auto_route: ^7.8.4
dev_dependencies:
build_runner: ^2.4.7
freezed: ^2.4.5
injectable_generator: ^2.4.1
auto_route_generator: ^7.3.2// domain/lib/repositories/user_repository.dart
abstract class IUserRepository {
Future<Either<Failure, User>> getUser(String id);
Future<Either<Failure, List<User>>> getUsers();
Future<Either<Failure, User>> updateUser(User user);
Future<Either<Failure, void>> deleteUser(String id);
}
// data/lib/repositories/user_repository_impl.dart
@Injectable(as: IUserRepository)
class UserRepositoryImpl implements IUserRepository {
final UserRemoteDataSource _remoteDataSource;
final NetworkInfo _networkInfo;
UserRepositoryImpl(this._remoteDataSource, this._networkInfo);
@override
Future<Either<Failure, User>> getUser(String id) async {
if (!await _networkInfo.isConnected) {
return Left(NetworkFailure('No internet connection'));
}
try {
final model = await _remoteDataSource.getUser(id);
return Right(model.toEntity());
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
}
}
}// domain/lib/usecases/user/get_user.dart
@injectable
class GetUser implements UseCase<User, String> {
final IUserRepository _repository;
GetUser(this._repository);
@override
Future<Either<Failure, User>> call(String params) async {
return await _repository.getUser(params);
}
}
// domain/lib/usecases/user/update_user.dart
@injectable
class UpdateUser implements UseCase<User, UpdateUserParams> {
final IUserRepository _repository;
UpdateUser(this._repository);
@override
Future<Either<Failure, User>> call(UpdateUserParams params) async {
return await _repository.updateUser(params.user);
}
}// apps/customer/lib/presentation/bloc/user/user_bloc.dart
@injectable
class UserBloc extends Bloc<UserEvent, UserState> {
final GetUser _getUser;
final UpdateUser _updateUser;
UserBloc(this._getUser, this._updateUser) : super(const UserState.initial()) {
on<UserEvent>((event, emit) async {
await event.map(
getUser: (e) async => await _handleGetUser(e, emit),
updateUser: (e) async => await _handleUpdateUser(e, emit),
);
});
}
Future<void> _handleGetUser(_GetUser event, Emitter<UserState> emit) async {
emit(const UserState.loading());
final result = await _getUser(event.id);
emit(result.fold(
(failure) => UserState.error(failure.message),
(user) => UserState.loaded(user),
));
}
Future<void> _handleUpdateUser(_UpdateUser event, Emitter<UserState> emit) async {
emit(const UserState.loading());
final result = await _updateUser(UpdateUserParams(event.user));
emit(result.fold(
(failure) => UserState.error(failure.message),
(user) => UserState.loaded(user),
));
}
}// apps/customer/lib/presentation/pages/user_profile/user_profile_page.dart
@RoutePage()
class UserProfilePage extends StatelessWidget {
final String userId;
const UserProfilePage({required this.userId});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => getIt<UserBloc>()
..add(UserEvent.getUser(userId)),
child: const UserProfileView(),
);
}
}
// apps/customer/lib/presentation/pages/user_profile/user_profile_view.dart
class UserProfileView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<UserBloc, UserState>(
builder: (context, state) => state.map(
initial: (_) => const SizedBox(),
loading: (_) => const LoadingIndicator(),
error: (state) => ErrorView(message: state.message),
loaded: (state) => UserProfileContent(user: state.user),
),
);
}
}
## Firebase Services
The application integrates with Firebase using a modular, clean architecture approach. Each Firebase service is encapsulated in its own class with proper error handling and configuration.
### Services
1. **Firebase Initializer**
- Handles Firebase initialization
- Configures services based on environment
- Provides error handling and logging
2. **Analytics Service**
- Log events
- Set user properties
- Track screen views
3. **Crashlytics Service**
- Record errors and crashes
- Set custom keys
- Set user identifiers
- Log messages
4. **Messaging Service**
- Handle push notifications
- Subscribe/unsubscribe to topics
- Get FCM token
- Handle background messages
5. **Remote Config Service**
- Fetch and activate config
- Get values (string, bool, int, double)
- Set defaults
- Configure fetch interval
6. **Storage Service**
- Upload files
- Delete files
- Get download URLs
- Handle metadata
7. **Dynamic Links Service**
- Create dynamic links
- Handle initial link
- Listen for link events
### Configuration
Each service can be enabled/disabled through the `FirebaseConfig` class:
```dart
const config = FirebaseConfig(
analyticsEnabled: true,
crashlyticsEnabled: true,
remoteConfigEnabled: true,
messagingEnabled: true,
storageEnabled: true,
dynamicLinksEnabled: true,
debugMode: false,
);-
Development
const config = FirebaseConfig.development(); // Analytics & Crashlytics disabled, debug mode enabled
-
Production
const config = FirebaseConfig.production(); // All services enabled, debug mode disabled
-
Test
const config = FirebaseConfig.test(); // All services disabled, debug mode enabled
All services use the Either type from dartz for error handling:
Future<Either<FirebaseFailure, T>> operation() async {
try {
// Operation logic
return Right(result);
} catch (e) {
return Left(FirebaseFailure.operationFailed());
}
}Services are registered using the injectable package:
@module
abstract class FirebaseModule {
@singleton
FirebaseConfig get firebaseConfig => const FirebaseConfig.production();
@singleton
FirebaseLogger get firebaseLogger => FirebaseLogger(firebaseConfig);
// ... other services
}-
Install dependencies:
flutter pub get
-
Generate code:
flutter pub run build_runner build
-
Configure Firebase:
- Add your
google-services.json(Android) andGoogleService-Info.plist(iOS) - Update Firebase configuration in
lib/sources/firebase/firebase_config.dart
- Add your
-
Run the app:
flutter run
Run tests with:
flutter testThe codebase includes unit tests for all Firebase services using mockito for mocking.
