# ๐Ÿ“ฆ Module Structure The Thunderbird for Android project is following a modularization approach, where the codebase is divided into multiple distinct modules. These modules encapsulate specific functionality and can be developed, tested, and maintained independently. This modular architecture promotes reusability, scalability, and maintainability of the codebase. Each module should be split into two main parts: **API** and **implementation**. This separation provides clear boundaries between what a module exposes to other modules and how it implements its functionality internally. When a feature is complex, it can be further split into sub modules, allowing for better organization and smaller modules for distinct functionalities within a feature domain. This approach promotes: - **Loose coupling**: Modules interact through well-defined interfaces - **Interchangeable implementations**: Different implementations can be swapped without affecting consumers - **Improved build times**: Reduces the scope of recompilation when changes are made - **Better testability**: Modules can be tested in isolation - **Clear ownership**: Teams can own specific modules ### ๐Ÿ“ API Module The API module defines the public contract that other modules can depend on. It should be stable, well-documented, and change infrequently. The API module contains: - **Public interfaces**: Contracts that define the module's capabilities - **Data models**: Entities that are part of the public API - **Constants and enums**: Shared constants and enumeration types - **Extension functions**: Utility functions that extend public types - **Navigation definitions**: Navigation routes and arguments The API module should be minimal and focused on defining the contract that other modules can depend on. It should not contain any implementation details. #### Naming Convention API modules should follow the naming convention: - `feature::api` for feature modules - `core::api` for core modules #### Example structure for a feature API module: ```bash feature:account:api โ”œโ”€โ”€ src/main/kotlin/net/thunderbird/feature/account/api โ”‚ โ”œโ”€โ”€ AccountManager.kt (interface) โ”‚ โ”œโ”€โ”€ Account.kt (entity) โ”‚ โ”œโ”€โ”€ AccountNavigation.kt (interface) โ”‚ โ”œโ”€โ”€ AccountType.kt (entity) โ”‚ โ””โ”€โ”€ AccountExtensions.kt (extension functions) ``` #### API Design Guidelines When designing APIs, follow these principles: - **Minimal surface area**: Expose only what is necessary - **Immutable data**: Use immutable data structures where possible - **Clear contracts**: Define clear method signatures with documented parameters and return values - **Error handling**: Define how errors are communicated (exceptions, result types, etc.) ### โš™๏ธ Implementation Module The implementation module depends on the API module but should not be depended upon by other modules (except for dependency injection setup). The implementation module contains: - **Interface implementations**: Concrete implementations of the interfaces defined in the API module - **Internal components**: Classes and functions used internally - **Data sources**: Repositories, database access, network clients - **UI components**: Screens, composables, and ViewModels #### Naming Convention Implementation modules should follow the naming convention: - `feature::impl` for standard implementations - `feature::impl-` for variant-specific implementations - `core::impl` for core module implementations #### Multiple Implementations When multiple implementations are needed, such as for different providers or platforms, they can be placed in separate modules and named accordingly: - `feature:account:impl-gmail` - Gmail-specific implementation - `feature:account:impl-yahoo` - Yahoo-specific implementation - `feature:account:impl-noop` - No-operation implementation for testing #### Example structure for a variant implementation: ```bash feature:account:impl-gmail โ”œโ”€โ”€ src/main/kotlin/app/thunderbird/feature/account/gmail โ”‚ โ””โ”€โ”€ GmailAccountManager.kt ``` #### Clean Architecture in Implementation Modules A complex feature implementation module should apply **Clean Architecture** principles, separating concerns into: - **UI Layer**: Compose UI components, ViewModels, and UI state management - **Domain Layer**: Use cases, domain models, and business logic - **Data Layer**: Repositories, data sources, and data mapping ```bash feature:account:impl โ”œโ”€โ”€ src/main/kotlin/app/thunderbird/feature/account/impl โ”‚ โ”œโ”€โ”€ data/ โ”‚ โ”‚ โ”œโ”€โ”€ repository/ โ”‚ โ”‚ โ”œโ”€โ”€ datasource/ โ”‚ โ”‚ โ””โ”€โ”€ mapper/ โ”‚ โ”œโ”€โ”€ domain/ โ”‚ โ”‚ โ”œโ”€โ”€ repository/ โ”‚ โ”‚ โ”œโ”€โ”€ entity/ โ”‚ โ”‚ โ””โ”€โ”€ usecase/ โ”‚ โ””โ”€โ”€ ui/ โ”‚ โ”œโ”€โ”€ AccountScreen.kt โ”‚ โ””โ”€โ”€ AccountViewModel.kt ``` #### Implementation Best Practices - **Internal visibility**: Use the `internal` modifier for classes and functions that should not be part of the public API - **Encapsulation**: Keep implementation details hidden from consumers - **Testability**: Design implementations to be easily testable - **Dependency injection**: Use constructor injection for dependencies - **Error handling**: Implement robust error handling according to API contracts - **Performance**: Consider performance implications of implementations - **Logging**: Include appropriate logging for debugging and monitoring ### ๐Ÿงช Testing Module Testing modules provide test implementations, utilities, and frameworks for testing other modules. They are essential for ensuring the quality and correctness of the codebase. #### Contents The testing module contains: - **Test utilities**: Helper functions and classes for testing - **Test frameworks**: Custom test frameworks and extensions - **Test fixtures**: Reusable test setups and teardowns - **Test matchers**: Custom matchers for assertions #### Naming Convention Testing modules should follow the naming convention: - `feature::testing` for feature-specific test utilities - `core::testing` for core test utilities - `:test` for module-specific tests #### Example structure for a testing module: ```bash feature:account:testing โ”œโ”€โ”€ src/main/kotlin/app/thunderbird/feature/account/testing โ”‚ โ”œโ”€โ”€ AccountTestUtils.kt โ”‚ โ””โ”€โ”€ AccountTestMatchers.kt ``` #### Testing Best Practices - **Reusability**: Create reusable test utilities and data factories - **Isolation**: Tests should be isolated and not depend on external systems - **Readability**: Tests should be easy to read and understand - **Maintainability**: Tests should be easy to maintain and update - **Coverage**: Tests should cover all critical paths and edge cases ### ๐Ÿค– Fake Module Fake modules provide alternative implementations of interfaces for testing, development, or demonstration purposes. They are simpler than the real implementations and are designed to be used in controlled environments. #### Contents The fake module contains: - **Fake implementations**: Simplified implementations of interfaces - **Generic test data**: Basic, reusable sample data for testing and demonstration - **In-memory data stores**: In-memory alternatives to real data stores - **Controlled behavior**: Implementations with predictable, configurable behavior - **Test doubles**: Mocks, stubs, and spies for testing > [!IMPORTANT] > Fake modules should be limited to the most generic data and implementations. Specific use cases or test setups should be part of the actual test, not the fake module. #### Naming Convention Fake modules should follow the naming convention: - `feature::fake` for feature-specific fake implementations - `core::fake` for core fake implementations #### Example structure for a fake module: ```bash feature:account:fake โ”œโ”€โ”€ src/main/kotlin/app/thunderbird/feature/account/fake โ”‚ โ”œโ”€โ”€ FakeAccountRepository.kt โ”‚ โ”œโ”€โ”€ FakeAccountDataSource.kt โ”‚ โ”œโ”€โ”€ InMemoryAccountStore.kt โ”‚ โ”œโ”€โ”€ FakeAccountManager.kt โ”‚ โ””โ”€โ”€ data/ โ”‚ โ”œโ”€โ”€ FakeAccountData.kt โ”‚ โ””โ”€โ”€ FakeAccountProfileData.kt ``` #### Fake Implementation Best Practices - **Simplicity**: Fake implementations should be simpler than real implementations - **Deterministic behavior**: Behavior should be predictable and controllable - **Configuration**: Allow configuration of behavior for different test scenarios - **Visibility**: Make internal state visible for testing assertions - **Performance**: Fake implementations should be fast for testing efficiency - **Generic test data**: Include basic, reusable test data that can be used across different tests - **Realistic but generic data**: Test data should be realistic enough to be useful but generic enough to be reused - **Separation of concerns**: Keep specific test scenarios and edge cases in the actual tests, not in the fake module ### ๐Ÿ”„ Common Module Common modules provide shared functionality that is used by multiple modules within a feature. They contain implementation details, utilities, and components that need to be shared between related modules but are not part of the public API. #### Contents The common module contains: - **Shared utilities**: Helper functions and classes used across related modules - **Internal implementations**: Implementation details shared between modules - **Shared UI components**: Reusable UI components specific to a feature domain - **Data repositories**: Shared data storage and access implementations - **Constants and resources**: Shared constants, strings, and other resources #### Naming Convention Common modules should follow the naming convention: - `feature::common` for feature-specific common code - `core::common` for core common code #### Example structure for a common module: ```bash feature:account:common โ”œโ”€โ”€ src/main/kotlin/net/thunderbird/feature/account/common โ”‚ โ”œโ”€โ”€ AccountCommonModule.kt โ”‚ โ”œโ”€โ”€ data/ โ”‚ โ”‚ โ””โ”€โ”€ InMemoryAccountStateRepository.kt โ”‚ โ”œโ”€โ”€ domain/ โ”‚ โ”‚ โ”œโ”€โ”€ AccountDomainContract.kt โ”‚ โ”‚ โ”œโ”€โ”€ input/ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ NumberInputField.kt โ”‚ โ”‚ โ””โ”€โ”€ entity/ โ”‚ โ”‚ โ”œโ”€โ”€ AccountState.kt โ”‚ โ”‚ โ”œโ”€โ”€ AccountDisplayOptions.kt โ”‚ โ”‚ โ””โ”€โ”€ AuthorizationState.kt โ”‚ โ””โ”€โ”€ ui/ โ”‚ โ”œโ”€โ”€ WizardNavigationBar.kt โ”‚ โ””โ”€โ”€ WizardNavigationBarState.kt ``` #### Common Module Best Practices - **Internal visibility**: Use the `internal` modifier for classes and functions that should not be part of the public API - **Clear organization**: Organize code into data, domain, and UI packages for better maintainability - **Shared contracts**: Define clear interfaces for functionality that will be implemented by multiple modules - **Reusable components**: Create UI components that can be reused across different screens within a feature - **Stateless where possible**: Design components to be stateless and receive state through parameters - **Minimal dependencies**: Keep dependencies to a minimum to avoid transitive dependency issues - **Documentation**: Document the purpose and usage of shared components - **Avoid leaking implementation details**: Don't expose implementation details that could create tight coupling ## ๐Ÿ”— Module Dependencies The module dependency diagram below illustrates how different modules interact with each other in the project, showing the dependencies and integration points between modules. ```mermaid graph TB subgraph APP[App Modules] direction TB APP_TB["`**:app-thunderbird**
Thunderbird for Android`"] APP_K9["`**:app-k9mail**
K-9 Mail`"] end subgraph COMMON[App Common Module] direction TB COMMON_APP["`**:app-common**
Integration Code`"] end subgraph FEATURE[Feature] direction TB FEATURE1[feature:account:api] FEATURE2[feature:account:impl] FEATURE3[Feature 2] FEATURE_K9[Feature K-9 Only] FEATURE_TB[Feature TfA Only] end subgraph CORE[Core] direction TB CORE1[Core 1] CORE2[Core 2] end subgraph LIBRARY[Library] direction TB LIB1[Library 1] LIB2[Library 2] end APP_K9 --> |depends on| COMMON_APP APP_TB --> |depends on| COMMON_APP COMMON_APP --> |integrates| FEATURE1 COMMON_APP --> |injects| FEATURE2 FEATURE2 --> FEATURE1 COMMON_APP --> |integrates| FEATURE3 APP_K9 --> |integrates| FEATURE_K9 APP_TB --> |integrates| FEATURE_TB FEATURE1 --> |uses| CORE1 FEATURE3 --> |uses| CORE2 FEATURE_TB --> |uses| CORE1 FEATURE_K9 --> |uses| LIB2 CORE2 --> |uses| LIB1 classDef app fill:#d9e9ff,stroke:#000000,color:#000000 classDef app_module fill:#4d94ff,stroke:#000000,color:#000000 classDef common fill:#e6e6e6,stroke:#000000,color:#000000 classDef common_module fill:#999999,stroke:#000000,color:#000000 classDef feature fill:#d9ffd9,stroke:#000000,color:#000000 classDef feature_module fill:#33cc33,stroke:#000000,color:#000000 classDef core fill:#e6cce6,stroke:#000000,color:#000000 classDef core_module fill:#cc99cc,stroke:#000000,color:#000000 classDef library fill:#fff0d0,stroke:#000000,color:#000000 classDef library_module fill:#ffaa33,stroke:#000000,color:#000000 classDef legacy fill:#ffe6e6,stroke:#000000,color:#000000 classDef legacy_module fill:#ff9999,stroke:#000000,color:#000000 linkStyle default stroke:#999,stroke-width:2px class APP app class APP_K9,APP_TB app_module class COMMON common class COMMON_APP common_module class FEATURE feature class FEATURE1,FEATURE2,FEATURE3 feature_module class FEATURE_K9 featureK9 class FEATURE_TB featureTB class CORE core class CORE1,CORE2 core_module class LIBRARY library class LIB1,LIB2 library_module ``` ### Module Interaction Patterns - **App Modules**: Depend on the App Common module for shared functionality and selectively integrate feature modules - **App Common**: Integrates various feature modules to provide a cohesive application - **Feature Modules**: Use core modules and libraries for their implementation, may depend on other feature API modules - **App-Specific Features**: Some features are integrated directly by specific apps (K-9 Mail or Thunderbird) ### Dependency Rules These rules must be strictly followed: 1. **One-Way Dependencies**: - Modules should not depend on each other in a circular manner - Dependencies should form a directed acyclic graph (DAG) 2. **API-Implementation Separation**: - Modules should depend only on API modules, not implementation modules - Implementation modules should be referenced only in dependency injection setup 3. **Feature Integration**: - Features should be integrated through the App Common module, which acts as a central hub - Direct dependencies between feature implementations should be avoided, or limited to API modules 4. **Dependency Direction**: - Dependencies should flow from app modules to common, then to features, and finally to core and libraries - Higher-level modules should depend on lower-level modules, not vice versa 5. **Minimal Dependencies**: - Each module should have the minimal set of dependencies required - Avoid unnecessary dependencies that could lead to bloat ### Dependency Management - **Explicit Dependencies**: All dependencies should be explicitly declared in the module's build file - **Transitive Dependencies**: Avoid relying on transitive dependencies - **Version Management**: Use centralized version management for dependencies - **Dependency Visibility**: Use appropriate visibility modifiers to limit access to implementation details ### Dependency Injection - Use Koin for dependency injection - Configure module dependencies in dedicated Koin modules - Inject API interfaces, not implementation classes - Use lazy injection where appropriate to improve startup performance ## ๐Ÿ“ Module Granularity Determining the right granularity for modules is crucial for maintainability and scalability. This section provides guidelines on when to create new modules and how to structure them. ### When to Create a New Module Create a new module when: 1. **Distinct Functionality**: The code represents a distinct piece of functionality with clear boundaries 2. **Reusability**: The functionality could be reused across multiple features or applications 3. **Build Performance**: Breaking down large modules improves build performance 4. **Testing**: Isolation improves testability ### When to Split a Module Split an existing module when: 1. **Size**: The module has grown too large (>10,000 lines of code as a rough guideline) 2. **Complexity**: The module has become too complex with many responsibilities 3. **Dependencies**: The module has too many dependencies 4. **Build Time**: The module takes too long to build ### When to Keep Modules Together Keep functionality in the same module when: 1. **Cohesion**: The functionality is highly cohesive and tightly coupled 2. **Small Size**: The functionality is small and simple 3. **Single Responsibility**: The functionality represents a single responsibility