Development Overview
This document details the software architecture strategy for TutorLM. Our approach is fundamentally geared towards creating a system that is modular, scalable, easily testable, and straightforward to maintain over time. To achieve these goals, we primarily employ a feature-first organizational structure, where the codebase is segmented by distinct application capabilities. This structure is further reinforced by principles inspired by Clean Architecture, which guide us in establishing a clear separation of concerns across different layers of the application.
The architecture is built upon several core tenets:
- Modularity: Features are developed as largely independent units.
- Separation of Concerns: Clear boundaries between UI logic, core business rules, and data handling.
- Dependency Rule: Dependencies point inwards—from Presentation to Domain, with Data serving Domain.
- Testability: Each layer/component can be verified in isolation.
- Scalability & Maintainability: New features integrate with minimal disruption; codebase remains organized and adaptable.
Architectural Layers and Code Structure
The application is conceptually divided into three primary layers:
- Presentation (Application) Layer
- Domain Layer
- Data Layer
Physically, the codebase reflects this through feature-specific folders, a central core
folder, and an app
folder, each with a distinct role.
Feature-Driven Development
The cornerstone of our organizational strategy is a feature-based approach. Each significant capability (e.g., Authentication, Session Management, Chat, User Profile) is encapsulated within its own folder (e.g., lib/auth/
, lib/session/
).
- UI, state management, and feature logic are co-located.
- Feature modules are highly independent, depending mainly on the
core
folder (for shared business logic, domain models, repository interfaces) and occasionally theapp
folder (for global configs, routing, shared state). - Direct dependencies between features are minimized, promoting decoupling and isolation.
The Presentation Layer
Responsible for everything the user sees and interacts with:
- UI Components: Widgets/Views in each feature folder (e.g.,
lib/session/view/
,lib/session/widgets/
). - State Management: Predominantly uses the Bloc library. Each feature has its own Blocs or Cubits (e.g.,
lib/session/bloc/
).- Blocs: For complex state with multiple events and transformations.
- Cubits: For simpler, lightweight state management.
- UI widgets listen to state changes (using
BlocBuilder
or similar) and rebuild accordingly. - User actions are dispatched as events to Blocs or as method calls to Cubits.
- The
app
folder (e.g.,lib/app/
):- Manages global state (e.g., authentication status, theme, localization) via a global
AppBloc
. - Handles root navigation setup and app lifecycle management.
- Manages global state (e.g., authentication status, theme, localization) via a global
Interaction Flow:
- UI reacts to Bloc/Cubit state changes.
- User interactions are channeled to state management components.
- Blocs/Cubits interact with repositories in the Domain Layer.
The Domain Layer
Encapsulated primarily within the core
folder. This is the application's business logic and core data structures.
- Domain Models (Entities): Plain Dart objects (e.g.,
User
,Session
,Course
inlib/core/models/
). - Repository Interfaces: Abstract contracts (e.g.,
AuthenticationRepository
,SessionRepository
inlib/core/repositories/
). - Use Cases: (Optional) Encapsulate specific business logic/user stories.
Key Characteristics:
- No Flutter-specific code.
- No widgets or Flutter dependencies.
- Platform-agnostic and highly testable.
The Data Layer
Responsible for data retrieval, storage, and communication with external/local sources. Provides concrete implementations for repository interfaces.
- Repository Implementations: In
lib/core/repositories/
, implement interfaces from the Domain Layer (e.g.,SessionRepositoryImpl
). - Data Sources:
- External Services/SDKs: Firebase Auth, Firestore, Storage, FCM, Google Sign-In, etc.
- Local Data Sources: (If needed) SQLite, shared preferences, etc.
- Data Transfer Objects (DTOs): Used for mapping between external data and Domain Models.
- Data Transformation: Converts raw data (e.g., Firestore docs, JSON) to Domain Models and vice versa.
Note: Even without a dedicated
data_sources
sub-folder, the Data Layer's responsibilities are fulfilled by repository implementations that interact with SDKs.
Understanding Interaction and Data Flow
The architecture promotes a clear, unidirectional flow of data and dependencies:
- User Interaction occurs in a UI widget.
- Event/Action is dispatched to a Bloc/Cubit.
- Bloc/Cubit communicates with a Repository Interface (Domain Layer).
- Repository Interface is routed to its Concrete Implementation (Data Layer) via Dependency Injection.
- Repository Implementation interacts with the Data Source (e.g., Firebase).
- Data is Retrieved/Transformed into Domain Models.
- Domain Model(s) or error are returned to the Bloc/Cubit.
- Bloc/Cubit emits a New State.
- UI Widgets rebuild to reflect the new state.
Facilitating Inter-Feature Communication
- Surface-Level (Navigational):
- Managed in
main.dart
or a router config file. - Features navigate via named routes (e.g.,
Navigator.pushNamed(context, '/featureB_view')
). - Avoids direct import dependencies between feature UIs.
- Managed in
- Deeper (Data-Driven):
- Mediated through shared repositories in the
core
folder. - Repositories are provided high in the widget tree (e.g., with
RepositoryProvider
). - Repositories may expose data as Streams (e.g.,
Stream<List<Session>>
). - Features subscribe to streams and react to updates, enabling a reactive, decoupled system.
- Mediated through shared repositories in the
Managing Cross-Cutting Concerns
- Dependency Injection:
- Uses
RepositoryProvider
andBlocProvider
fromflutter_bloc
for dependency management and testability.
- Uses
- Consistent Styling & Theming:
- Centralized in
lib/core/style/
(e.g.,themes.dart
).
- Centralized in
- Shared Widgets:
- Common UI elements in
lib/core/widgets/
.
- Common UI elements in
- Utilities & Formatters:
- Helper functions in
lib/core/utils/
.
- Helper functions in
- Error Handling:
- Repositories return results indicating success/failure (e.g.,
Either
, customResult
, or typed exceptions). - Blocs/Cubits translate errors into UI states (e.g.,
ErrorState
).
- Repositories return results indicating success/failure (e.g.,
- Application Configuration:
- Centralized (e.g.,
firebase_options.dart
, environment configs).
- Centralized (e.g.,
Advantages of This Architectural Approach
- Modularity & Decoupling: Features are well-isolated and easy to manage.
- Testability:
- Blocs/Cubits can be unit-tested with mocked repositories.
- Domain logic is testable in isolation.
- Repository implementations can be tested with mocked data sources.
- Widget testing is simplified by providing mock Blocs/Cubits.
- Scalability: New features or changes have localized impact.
- Maintainability: Clear structure reduces cognitive load and risk of regressions.
- Parallel Development: Teams can work on different features concurrently with minimal conflicts.
Visualizing the High-Level Architecture of TutorLM
Potential Considerations and Trade-offs
- Initial Setup Complexity:
- Higher than monolithic approaches; more boilerplate and a steeper learning curve.
- For Small/Simple Projects:
- Strict layering may be overkill, but pays off as the app grows.