Repo created
This commit is contained in:
parent
75dc487a7a
commit
39c29d175b
6317 changed files with 388324 additions and 2 deletions
657
docs/architecture/README.md
Normal file
657
docs/architecture/README.md
Normal file
|
|
@ -0,0 +1,657 @@
|
|||
# 🏗️ Architecture
|
||||
|
||||
The application follows a modular architecture with clear separation between different layers and components. The architecture is designed to support both the Thunderbird for Android and K-9 Mail applications while maximizing code reuse, maintainability and enable adoption of Kotlin Multiplatform in the future.
|
||||
|
||||
## 🔑 Key Architectural Principles
|
||||
|
||||
- **🚀 Multi platform Compatibility**: The architecture is designed to support future Kotlin Multiplatform adoption
|
||||
- **📱 Offline-First**: The application is designed to work offline with local data storage and synchronization with remote servers
|
||||
- **🧩 Modularity**: The application is divided into distinct modules with clear responsibilities
|
||||
- **🔀 Separation of Concerns**: Each module focuses on a specific aspect of the application
|
||||
- **⬇️ Dependency Inversion**: Higher-level modules do not depend on lower-level modules directly
|
||||
- **🎯 Single Responsibility**: Each component has a single responsibility
|
||||
- **🔄 API/Implementation Separation**: Clear separation between public APIs and implementation details
|
||||
- **🧹 Clean Architecture**: Separation of UI, domain, and data layers
|
||||
- **🧪 Testability**: The architecture facilitates comprehensive testing at all levels
|
||||
|
||||
## 📝 Architecture Decision Records
|
||||
|
||||
The [Architecture Decision Records](adr/README.md) document the architectural decisions made during the development of the
|
||||
project, providing context and rationale for key technical choices. Reading through these decisions will improve your
|
||||
contributions and ensure long-term maintainability of the project.
|
||||
|
||||
## 📦 Module Structure
|
||||
|
||||
The application is organized into several module types:
|
||||
|
||||
- **📱 App Modules**: `app-thunderbird` and `app-k9mail` - Application entry points
|
||||
- **🔄 App Common**: `app-common` - Shared code between applications
|
||||
- **✨ Feature Modules**: `feature:*` - Independent feature modules
|
||||
- **🧰 Core Modules**: `core:*` - Foundational components and utilities used across multiple features
|
||||
- **📚 Library Modules**: `library:*` - Specific implementations for reuse
|
||||
- **🔙 Legacy Modules**: Legacy code being gradually migrated
|
||||
|
||||
For more details on the module organization and structure, see the [Module Organization](module-organization.md) and
|
||||
[Module Structure](module-structure.md) documents.
|
||||
|
||||
## 🧩 Architectural Patterns
|
||||
|
||||
The architecture follows several key patterns to ensure maintainability, testability, and separation of concerns:
|
||||
|
||||
### 🔄 API/Implementation Separation
|
||||
|
||||
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:
|
||||
|
||||
- **📝 API**: Public interfaces, models, and contracts
|
||||
- **⚙️ Implementation**: Concrete implementations of the interfaces
|
||||
|
||||
This separation provides clear boundaries, improves testability, and enables flexibility.
|
||||
|
||||
See [API Module](module-structure.md#-api-module) and
|
||||
[Implementation Module](module-structure.md#-implementation-module) for more details.
|
||||
|
||||
### Clean Architecture
|
||||
|
||||
Thunderbird for Android uses **Clean Architecture** with three main layers (UI, domain, and data) to break down complex
|
||||
feature implementation into manageable components. Each layer has a specific responsibility:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph UI[UI Layer]
|
||||
UI_COMPONENTS[UI Components]
|
||||
VIEW_MODEL[ViewModels]
|
||||
end
|
||||
|
||||
subgraph DOMAIN["Domain Layer"]
|
||||
USE_CASE[Use Cases]
|
||||
REPO[Repositories]
|
||||
end
|
||||
|
||||
subgraph DATA[Data Layer]
|
||||
DATA_SOURCE[Data Sources]
|
||||
API[API Clients]
|
||||
DB[Local Database]
|
||||
end
|
||||
|
||||
UI_COMPONENTS --> VIEW_MODEL
|
||||
VIEW_MODEL --> USE_CASE
|
||||
USE_CASE --> REPO
|
||||
REPO --> DATA_SOURCE
|
||||
DATA_SOURCE --> API
|
||||
DATA_SOURCE --> DB
|
||||
|
||||
classDef ui_layer fill:#d9e9ff,stroke:#000000,color:#000000
|
||||
classDef ui_class fill:#4d94ff,stroke:#000000,color:#000000
|
||||
classDef domain_layer fill:#d9ffd9,stroke:#000000,color:#000000
|
||||
classDef domain_class fill:#33cc33,stroke:#000000,color:#000000
|
||||
classDef data_layer fill:#ffe6cc,stroke:#000000,color:#000000
|
||||
classDef data_class fill:#ffaa33,stroke:#000000,color:#000000
|
||||
|
||||
linkStyle default stroke:#999,stroke-width:2px
|
||||
|
||||
class UI ui_layer
|
||||
class UI_COMPONENTS,VIEW_MODEL ui_class
|
||||
class DOMAIN domain_layer
|
||||
class USE_CASE,REPO domain_class
|
||||
class DATA data_layer
|
||||
class DATA_SOURCE,API,DB data_class
|
||||
```
|
||||
|
||||
#### 🖼️ UI Layer (Presentation)
|
||||
|
||||
The UI layer is responsible for displaying data to the user and handling user interactions.
|
||||
|
||||
**Key Components:**
|
||||
- **🎨 [Compose UI](ui-architecture.md#-screens)**: Screen components built with Jetpack Compose
|
||||
- **🧠 [ViewModels](ui-architecture.md#-viewmodel)**: Manage UI state and handle UI events
|
||||
- **📊 [UI State](ui-architecture.md#-state)**: Immutable data classes representing the UI state
|
||||
- **🎮 [Events](ui-architecture.md#-events)**: User interactions or system events that trigger state changes
|
||||
- **🔔 [Effects](ui-architecture.md#effects)**: One-time side effects like navigation or showing messages
|
||||
|
||||
**Pattern: Model-View-Intent (MVI)**
|
||||
|
||||
- **📋 Model**: UI state representing the current state of the screen
|
||||
- **👁️ View**: Compose UI that renders the state
|
||||
- **🎮 Event**: User interactions that trigger state changes (equivalent to "Intent" in standard MVI)
|
||||
- **🔔 Effect**: One-time side effects like navigation or notifications
|
||||
|
||||
#### 🧠 Domain Layer (Business Logic)
|
||||
|
||||
The domain layer contains the business logic and rules of the application. It is independent of the UI and data layers,
|
||||
allowing for easy testing and reuse.
|
||||
|
||||
**Key Components:**
|
||||
- **⚙️ Use Cases**: Encapsulate business logic operations
|
||||
- **📋 Domain Models**: Represent business entities
|
||||
- **📝 Repository Interfaces**: Define data access contracts
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph DOMAIN[Domain Layer]
|
||||
USE_CASE[Use Cases]
|
||||
MODEL[Domain Models]
|
||||
REPO_API[Repository Interfaces]
|
||||
end
|
||||
|
||||
subgraph DATA[Data Layer]
|
||||
REPO_IMPL[Repository Implementations]
|
||||
end
|
||||
|
||||
USE_CASE --> |uses| REPO_API
|
||||
USE_CASE --> |uses| MODEL
|
||||
REPO_API --> |uses| MODEL
|
||||
REPO_IMPL --> |implements| REPO_API
|
||||
REPO_IMPL --> |uses| MODEL
|
||||
|
||||
classDef domain_layer fill:#d9ffd9,stroke:#000000,color:#000000
|
||||
classDef domain_class fill:#33cc33,stroke:#000000,color:#000000
|
||||
classDef data_layer fill:#ffe6cc,stroke:#000000,color:#000000
|
||||
classDef data_class fill:#ffaa33,stroke:#000000,color:#000000
|
||||
|
||||
linkStyle default stroke:#999,stroke-width:2px
|
||||
|
||||
class DOMAIN domain_layer
|
||||
class USE_CASE,REPO_API,MODEL domain_class
|
||||
class DATA data_layer
|
||||
class REPO_IMPL data_class
|
||||
```
|
||||
|
||||
#### 💾 Data Layer
|
||||
|
||||
The data layer is responsible for data retrieval, storage, and synchronization.
|
||||
|
||||
**Key Components:**
|
||||
- **📦 Repository implementations**: Implement repository interfaces from the domain layer
|
||||
- **🔌 Data Sources**: Provide data from specific sources (API, database, preferences)
|
||||
- **📄 Data Transfer Objects**: Represent data at the data layer
|
||||
|
||||
**Pattern: Data Source Pattern**
|
||||
- 🔍 Abstracts data sources behind a clean API
|
||||
- Maps data between domain models and data transfer objects
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph DOMAIN[Domain Layer]
|
||||
REPO_API[Repository]
|
||||
end
|
||||
|
||||
subgraph DATA[Data Layer]
|
||||
REPO_IMPL[Repository implementations]
|
||||
RDS[Remote Data Sources]
|
||||
LDS[Local Data Sources]
|
||||
MAPPER[Data Mappers]
|
||||
DTO[Data Transfer Objects]
|
||||
end
|
||||
|
||||
REPO_IMPL --> |implements| REPO_API
|
||||
REPO_IMPL --> RDS
|
||||
REPO_IMPL --> LDS
|
||||
REPO_IMPL --> MAPPER
|
||||
RDS --> MAPPER
|
||||
LDS --> MAPPER
|
||||
MAPPER --> DTO
|
||||
|
||||
classDef domain_layer fill:#d9ffd9,stroke:#000000,color:#000000
|
||||
classDef domain_class fill:#33cc33,stroke:#000000,color:#000000
|
||||
classDef data_layer fill:#ffe6cc,stroke:#000000,color:#000000
|
||||
classDef data_class fill:#ffaa33,stroke:#000000,color:#000000
|
||||
|
||||
linkStyle default stroke:#999,stroke-width:2px
|
||||
|
||||
class DOMAIN domain_layer
|
||||
class REPO_API domain_class
|
||||
class DATA data_layer
|
||||
class REPO_IMPL,RDS,LDS,MAPPER,DTO data_class
|
||||
```
|
||||
|
||||
### 🔄 Immutability
|
||||
|
||||
Immutability means that once an object is created, it cannot be changed. Instead of modifying existing objects, new objects are created with the desired changes. In the context of UI state, this means that each state object represents a complete snapshot of the UI at a specific point in time.
|
||||
|
||||
**Why is Immutability Important?**
|
||||
|
||||
Immutability provides several benefits:
|
||||
- **Predictability**: With immutable state, the UI can only change when a new state object is provided, making the flow of data more predictable and easier to reason about.
|
||||
- **Debugging**: Each state change creates a new state object, making it easier to track changes and debug issues by comparing state objects.
|
||||
- **Concurrency**: Immutable objects are thread-safe by nature, eliminating many concurrency issues.
|
||||
- **Performance**: While creating new objects might seem inefficient, modern frameworks optimize this process, and the benefits of immutability often outweigh the costs.
|
||||
- **Time-travel debugging**: Immutability enables storing previous states, allowing developers to "time travel" back to previous application states during debugging.
|
||||
|
||||
## 🎨 UI Architecture
|
||||
|
||||
The UI is built using Jetpack Compose with a component-based architecture following our modified Model-View-Intent (MVI) pattern. This architecture provides a unidirectional data flow, clear separation of concerns, and improved testability.
|
||||
|
||||
For detailed information about the UI architecture and theming, see the [UI Architecture](ui-architecture.md) and
|
||||
[Theme System](theme-system.md) documents.
|
||||
|
||||
## 📱 Offline-First Approach
|
||||
|
||||
The application implements an offline-first Approach to provide a reliable user experience regardless of network conditions:
|
||||
|
||||
- 💾 Local database as the single source of truth
|
||||
- 🔄 Background synchronization with remote servers
|
||||
- 📋 Operation queueing for network operations
|
||||
- 🔀 Conflict resolution for data modified both locally and remotely
|
||||
|
||||
#### Implementation Approach
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph UI[UI Layer]
|
||||
VIEW_MODEL[ViewModel]
|
||||
end
|
||||
|
||||
subgraph DOMAIN[Domain Layer]
|
||||
USE_CASE[Use Cases]
|
||||
end
|
||||
|
||||
subgraph DATA[Data Layer]
|
||||
subgraph SYNC[Synchronization]
|
||||
SYNC_MANAGER[Sync Manager]
|
||||
SYNC_QUEUE[Sync Queue]
|
||||
end
|
||||
REPO[Repository]
|
||||
LOCAL[Local Data Source]
|
||||
REMOTE[Remote Data Source]
|
||||
end
|
||||
|
||||
VIEW_MODEL --> USE_CASE
|
||||
USE_CASE --> REPO
|
||||
SYNC_MANAGER --> LOCAL
|
||||
SYNC_MANAGER --> REMOTE
|
||||
SYNC_MANAGER --> SYNC_QUEUE
|
||||
REPO --> LOCAL
|
||||
REPO --> REMOTE
|
||||
REPO --> SYNC_MANAGER
|
||||
REPO ~~~ SYNC
|
||||
|
||||
classDef ui_layer fill:#d9e9ff,stroke:#000000,color:#000000
|
||||
classDef ui_class fill:#4d94ff,stroke:#000000,color:#000000
|
||||
classDef domain_layer fill:#d9ffd9,stroke:#000000,color:#000000
|
||||
classDef domain_class fill:#33cc33,stroke:#000000,color:#000000
|
||||
classDef data_layer fill:#ffe6cc,stroke:#000000,color:#000000
|
||||
classDef data_class fill:#ffaa33,stroke:#000000,color:#000000
|
||||
classDef sync_layer fill:#e6cce6,stroke:#000000,color:#000000
|
||||
classDef sync_class fill:#cc99cc,stroke:#000000,color:#000000
|
||||
|
||||
linkStyle default stroke:#999,stroke-width:2px
|
||||
|
||||
class UI ui_layer
|
||||
class VIEW_MODEL ui_class
|
||||
class DOMAIN domain_layer
|
||||
class USE_CASE domain_class
|
||||
class DATA data_layer
|
||||
class REPO,LOCAL,REMOTE data_class
|
||||
class SYNC sync_layer
|
||||
class SYNC_MANAGER,SYNC_API,SYNC_QUEUE sync_class
|
||||
```
|
||||
|
||||
The offline-first approach is implemented across all layers of the application:
|
||||
|
||||
1. **💾 Data Layer**:
|
||||
- 📊 Local database as the primary data source
|
||||
- 🌐 Remote data source for server communication
|
||||
- 📦 Repository pattern to coordinate between data sources
|
||||
- 🔄 Synchronization manager to handle data syncing
|
||||
2. **🧠 Domain Layer**:
|
||||
- ⚙️ Use cases handle both online and offline scenarios
|
||||
- 📝 Business logic accounts for potential network unavailability
|
||||
- 📋 Domain models represent data regardless of connectivity state
|
||||
3. **🖼️ UI Layer**:
|
||||
- 🧠 ViewModels expose UI state that reflects connectivity status
|
||||
- 🚦 UI components display appropriate indicators for offline mode
|
||||
- 👆 User interactions are designed to work regardless of connectivity
|
||||
|
||||
## 💉 Dependency Injection
|
||||
|
||||
The application uses [Koin](https://insert-koin.io/) for dependency injection, with modules organized by feature:
|
||||
|
||||
- **📱 App Modules**: Configure application-wide dependencies
|
||||
- **🔄 App Common**: Shared dependencies between applications
|
||||
- **✨ Feature Modules**: Configure feature-specific dependencies
|
||||
- **🧰 Core Modules**: Configure core dependencies
|
||||
|
||||
```kotlin
|
||||
// Example Koin module for a feature
|
||||
val featureModule = module {
|
||||
viewModel { FeatureViewModel(get()) }
|
||||
single<FeatureRepository> { FeatureRepositoryImpl(get(), get()) }
|
||||
single<FeatureUseCase> { FeatureUseCaseImpl(get()) }
|
||||
single<FeatureApiClient> { FeatureApiClientImpl() }
|
||||
}
|
||||
```
|
||||
|
||||
## 🔄 Cross-Cutting Concerns
|
||||
|
||||
Cross-cutting concerns are aspects of the application that affect multiple features and cannot be cleanly handled
|
||||
individually for every feature. These concerns require consistent implementation throughout the codebase to ensure
|
||||
maintainability an reliability.
|
||||
|
||||
In Thunderbird for Android, several cross-cutting concerns are implemented as dedicated core modules to provide
|
||||
standardized solutions that can be reused across the application:
|
||||
|
||||
- **⚠️ Error Handling**: Comprehensive error handling (`core/outcome`) transforms exceptions into domain-specific errors and provides user-friendly feedback.
|
||||
- **📋 Logging**: Centralized logging system (`core/logging`) ensures consistent log formatting, levels, and storage.
|
||||
- **🔒 Security**: Modules like `core/security` handle encryption, authentication, and secure data storage.
|
||||
|
||||
Work in progress:
|
||||
- **🔐 Encryption**: The `core/crypto` module provides encryption and decryption utilities for secure data handling.
|
||||
- **📦 Feature Flags**: The `core/feature-flags` module manages feature toggles and experimental features.
|
||||
- **🔄 Synchronization**: The `core/sync` module manages background synchronization, conflict resolution, and offline-first behavior.
|
||||
- **🛠️ Configuration Management**: Centralized handling of application settings and environment-specific configurations.
|
||||
|
||||
By implementing these concerns as core modules, the application achieves a clean and modular architecture that is easier to maintain and extend.
|
||||
|
||||
### ⚠️ Error Handling
|
||||
|
||||
The application implements a comprehensive error handling strategy across all layers. We favor using the Outcome pattern
|
||||
over exceptions for expected error conditions, while exceptions are reserved for truly exceptional situations that
|
||||
indicate programming errors or unrecoverable system failures.
|
||||
|
||||
- 🧠 **Domain Errors**: Encapsulate business logic errors as sealed classes, ensuring clear representation of specific
|
||||
error cases.
|
||||
- 💾 **Data Errors**: Transform network or database exceptions into domain-specific errors using result patterns in repository implementations.
|
||||
- 🖼️ **UI Error Handling**: Provide user-friendly error feedback by:
|
||||
- Mapping domain errors to UI state in ViewModels.
|
||||
- Displaying actionable error states in Compose UI components.
|
||||
- Offering retry options for network connectivity issues.
|
||||
|
||||
> [!NOTE]
|
||||
> Exceptions should be used sparingly. Favor the Outcome pattern and sealed classes for predictable error conditions to
|
||||
> enhance maintainability and clarity.
|
||||
|
||||
#### 🛠️ How to Implement Error Handling
|
||||
|
||||
When implementing error handling in your code:
|
||||
|
||||
1. **Define domain-specific errors** as sealed classes in your feature's domain layer:
|
||||
|
||||
```kotlin
|
||||
sealed class AccountError {
|
||||
data class AuthenticationFailed(val reason: String) : AccountError()
|
||||
data class NetworkError(val exception: Exception) : AccountError()
|
||||
data class ValidationError(val field: String, val message: String) : AccountError()
|
||||
}
|
||||
```
|
||||
2. **Use result patterns (Outcome)** instead of exceptions for error handling:
|
||||
|
||||
```kotlin
|
||||
// Use the Outcome class for representing success or failure
|
||||
sealed class Outcome<out T, out E> {
|
||||
data class Success<T>(val value: T) : Outcome<T, Nothing>()
|
||||
data class Failure<E>(val error: E) : Outcome<Nothing, E>()
|
||||
}
|
||||
```
|
||||
3. **Transform external errors** into domain errors in your repositories using result patterns:
|
||||
|
||||
```kotlin
|
||||
// Return Outcome instead of throwing exceptions
|
||||
fun authenticate(credentials: Credentials): Outcome<AuthResult, AccountError> {
|
||||
return try {
|
||||
val result = apiClient.authenticate(credentials)
|
||||
Outcome.Success(result)
|
||||
} catch (e: HttpException) {
|
||||
val error = when (e.code()) {
|
||||
401 -> AccountError.AuthenticationFailed("Invalid credentials")
|
||||
else -> AccountError.NetworkError(e)
|
||||
}
|
||||
logger.error(e) { "Authentication failed: ${error::class.simpleName}" }
|
||||
Outcome.Failure(error)
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) { "Authentication failed with unexpected error" }
|
||||
Outcome.Failure(AccountError.NetworkError(e))
|
||||
}
|
||||
}
|
||||
```
|
||||
4. **Handle errors in Use Cases** by propagating the Outcome:
|
||||
|
||||
```kotlin
|
||||
class LoginUseCase(
|
||||
private val accountRepository: AccountRepository,
|
||||
private val credentialValidator: CredentialValidator,
|
||||
) {
|
||||
fun execute(credentials: Credentials): Outcome<AuthResult, AccountError> {
|
||||
// Validate input first
|
||||
val validationResult = credentialValidator.validate(credentials)
|
||||
if (validationResult is ValidationResult.Failure) {
|
||||
return Outcome.Failure(
|
||||
AccountError.ValidationError(
|
||||
field = validationResult.field,
|
||||
message = validationResult.message
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Proceed with authentication
|
||||
return accountRepository.authenticate(credentials)
|
||||
}
|
||||
}
|
||||
```
|
||||
5. **Handle outcomes in ViewModels** and transform them into UI state:
|
||||
|
||||
```kotlin
|
||||
viewModelScope.launch {
|
||||
val outcome = loginUseCase.execute(credentials)
|
||||
|
||||
when (outcome) {
|
||||
is Outcome.Success -> {
|
||||
_uiState.update { it.copy(isLoggedIn = true) }
|
||||
}
|
||||
is Outcome.Failure -> {
|
||||
val errorMessage = when (val error = outcome.error) {
|
||||
is AccountError.AuthenticationFailed ->
|
||||
stringProvider.getString(R.string.error_authentication_failed, error.reason)
|
||||
is AccountError.NetworkError ->
|
||||
stringProvider.getString(R.string.error_network, error.exception.message)
|
||||
is AccountError.ValidationError ->
|
||||
stringProvider.getString(R.string.error_validation, error.field, error.message)
|
||||
}
|
||||
_uiState.update { it.copy(error = errorMessage) }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
6. **Always log errors** for debugging purposes:
|
||||
|
||||
```kotlin
|
||||
// Logging is integrated into the Outcome pattern
|
||||
fun fetchMessages(): Outcome<List<Message>, MessageError> {
|
||||
return try {
|
||||
val messages = messageService.fetchMessages()
|
||||
logger.info { "Successfully fetched ${messages.size} messages" }
|
||||
Outcome.Success(messages)
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) { "Failed to fetch messages" }
|
||||
Outcome.Failure(MessageError.FetchFailed(e))
|
||||
}
|
||||
}
|
||||
```
|
||||
7. **Compose multiple operations** that return Outcomes:
|
||||
|
||||
```kotlin
|
||||
fun synchronizeAccount(): Outcome<SyncResult, SyncError> {
|
||||
// First operation
|
||||
val messagesOutcome = fetchMessages()
|
||||
if (messagesOutcome is Outcome.Failure) {
|
||||
return Outcome.Failure(SyncError.MessageSyncFailed(messagesOutcome.error))
|
||||
}
|
||||
|
||||
// Second operation using the result of the first
|
||||
val messages = messagesOutcome.getOrNull()!!
|
||||
val folderOutcome = updateFolders(messages)
|
||||
if (folderOutcome is Outcome.Failure) {
|
||||
return Outcome.Failure(SyncError.FolderUpdateFailed(folderOutcome.error))
|
||||
}
|
||||
|
||||
// Return success with combined results
|
||||
return Outcome.Success(
|
||||
SyncResult(
|
||||
messageCount = messages.size,
|
||||
folderCount = folderOutcome.getOrNull()!!.size
|
||||
)
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 📝 Logging
|
||||
|
||||
The application uses a structured logging system with a well-defined API:
|
||||
|
||||
- 📊 **Logging Architecture**:
|
||||
- Core logging API (`core/logging/api`) defines interfaces like `Logger` and `LogSink`
|
||||
- Multiple implementations (composite, console) allow for flexible logging targets
|
||||
- Composite implementation enables logging to multiple sinks simultaneously
|
||||
- 🔄 **Logger vs. Sink**:
|
||||
- **Logger**: The front-facing interface that application code interacts with to create log entries
|
||||
- Provides methods for different log levels (verbose, debug, info, warn, error)
|
||||
- Handles the creation of log events with appropriate metadata (timestamp, tag, etc.)
|
||||
- Example: `DefaultLogger` implements the `Logger` interface and delegates to a `LogSink`
|
||||
- **LogSink**: The back-end component that receives log events and determines how to process them
|
||||
- Defines where and how log messages are actually stored or displayed
|
||||
- Filters log events based on configured log levels
|
||||
- Can be implemented in various ways (console output, file storage, remote logging service)
|
||||
- Multiple sinks can be used simultaneously via composite pattern
|
||||
- 📋 **Log Levels**:
|
||||
- `VERBOSE`: Most detailed log level for debugging
|
||||
- `DEBUG`: Detailed information for diagnosing problems
|
||||
- `INFO`: General information about application flow
|
||||
- `WARN`: Potential issues that don't affect functionality
|
||||
- `ERROR`: Issues that affect functionality but don't crash the application
|
||||
|
||||
#### 🛠️ How to Implement Logging
|
||||
|
||||
When adding logging to your code:
|
||||
|
||||
1. **Inject a Logger** into your class:
|
||||
|
||||
```kotlin
|
||||
class AccountRepository(
|
||||
private val apiClient: ApiClient,
|
||||
private val logger: Logger,
|
||||
) {
|
||||
// Repository implementation
|
||||
}
|
||||
```
|
||||
2. **Choose the appropriate log level** based on the importance of the information:
|
||||
- Use `verbose` for detailed debugging information (only visible in debug builds)
|
||||
- Use `debug` for general debugging information
|
||||
- Use `info` for important events that should be visible in production
|
||||
- Use `warn` for potential issues that don't affect functionality
|
||||
- Use `error` for issues that affect functionality
|
||||
3. **Use lambda syntax** to avoid string concatenation when logging isn't needed:
|
||||
|
||||
```kotlin
|
||||
// Good - string is only created if this log level is enabled
|
||||
logger.debug { "Processing message with ID: $messageId" }
|
||||
|
||||
// Avoid - string is always created even if debug logging is disabled
|
||||
logger.debug("Processing message with ID: " + messageId)
|
||||
```
|
||||
4. **Include relevant context** in log messages:
|
||||
|
||||
```kotlin
|
||||
logger.info { "Syncing account: ${account.email}, folders: ${folders.size}" }
|
||||
```
|
||||
5. **Log exceptions** with the appropriate level and context:
|
||||
|
||||
```kotlin
|
||||
try {
|
||||
apiClient.fetchMessages()
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) { "Failed to fetch messages for account: ${account.email}" }
|
||||
throw MessageSyncError.FetchFailed(e)
|
||||
}
|
||||
```
|
||||
6. **Use tags** for better filtering when needed:
|
||||
|
||||
```kotlin
|
||||
private val logTag = LogTag("AccountSync")
|
||||
|
||||
fun syncAccount() {
|
||||
logger.info(logTag) { "Starting account sync for: ${account.email}" }
|
||||
}
|
||||
```
|
||||
|
||||
### 🔒 Security
|
||||
|
||||
Security is a critical aspect of an email client. The application implements:
|
||||
|
||||
- 🔐 **Data Encryption**:
|
||||
- End-to-end encryption using OpenPGP (via the `legacy/crypto-openpgp` module)
|
||||
- Classes like `EncryptionDetector` and `OpenPgpEncryptionExtractor` handle encrypted emails
|
||||
- Local storage encryption for sensitive data like account credentials
|
||||
- 🔑 **Authentication**:
|
||||
- Support for various authentication types (OAuth, password, client certificate)
|
||||
- Secure token storage and management
|
||||
- Authentication error handling and recovery
|
||||
- 🛡️ **Network Security**:
|
||||
- TLS for all network connections with certificate validation
|
||||
- Certificate pinning for critical connections
|
||||
- Protection against MITM attacks
|
||||
|
||||
> [!NOTE]
|
||||
> This section is a work in progress. The security architecture is being developed and will be documented in detail
|
||||
> as it evolves.
|
||||
|
||||
#### 🛠️ How to Implement Security
|
||||
|
||||
When implementing security features in your code:
|
||||
|
||||
1. **Never store sensitive data in plain text**:
|
||||
|
||||
```kotlin
|
||||
// Bad - storing password in plain text
|
||||
sharedPreferences.putString("password", password)
|
||||
|
||||
// Good - use the secure credential storage
|
||||
val credentialStorage = get<CredentialStorage>()
|
||||
credentialStorage.storeCredentials(accountUuid, credentials)
|
||||
```
|
||||
2. **Use encryption for sensitive data**:
|
||||
|
||||
```kotlin
|
||||
// For data that needs to be stored encrypted
|
||||
val encryptionManager = get<EncryptionManager>()
|
||||
val encryptedData = encryptionManager.encrypt(sensitiveData)
|
||||
database.storeEncryptedData(encryptedData)
|
||||
```
|
||||
3. **Validate user input** to prevent injection attacks:
|
||||
|
||||
```kotlin
|
||||
// Validate input before using it
|
||||
if (!InputValidator.isValidEmailAddress(userInput)) {
|
||||
throw ValidationError("Invalid email address")
|
||||
}
|
||||
```
|
||||
4. **Use secure network connections**:
|
||||
|
||||
```kotlin
|
||||
// The networking modules enforce TLS by default
|
||||
// Make sure to use the provided clients rather than creating your own
|
||||
val networkClient = get<NetworkClient>()
|
||||
```
|
||||
|
||||
## 🧪 Testing Strategy
|
||||
|
||||
The architecture supports comprehensive testing:
|
||||
|
||||
- **🔬 Unit Tests**: Test individual components in isolation
|
||||
- **🔌 Integration Tests**: Test interactions between components
|
||||
- **📱 UI Tests**: Test the UI behavior and user flows
|
||||
|
||||
See the [Testing guide](../contributing/testing-guide.md) document for more details on how to write and run tests
|
||||
for the application.
|
||||
|
||||
## 🔙 Legacy Integration
|
||||
|
||||
The application includes legacy code that is gradually being migrated to the new architecture:
|
||||
- **📦 Legacy Modules**: Contain code from the original K-9 Mail application
|
||||
- **🔄 Migration Strategy**: Gradual migration to the new architecture
|
||||
- **🔌 Integration Points**: Clear interfaces between legacy and new code
|
||||
|
||||
For more details on the legacy integration, see the [Legacy Integration](legacy-module-integration.md) document.
|
||||
|
||||
## 🔄 User Flows
|
||||
|
||||
The [User Flows](user-flows.md) provides visual representations of typical user flows through the application, helping to understand how different components interact.
|
||||
36
docs/architecture/adr/0000-adr-template.md
Normal file
36
docs/architecture/adr/0000-adr-template.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# Descriptive Title in Title Case
|
||||
|
||||
- Issue: [#NNNN](https://github.com/thunderbird/thunderbird-android/issues/NNNN)
|
||||
- Pull Request: [#NNNN](https://github.com/thunderbird/thunderbird-android/pull/NNNN)
|
||||
|
||||
<!-- optional in case there are follow-up issues
|
||||
- Tracking Issue: [#NNNN](https://github.com/thunderbird/thunderbird-android/issues/NNNN)
|
||||
-->
|
||||
|
||||
## Status
|
||||
|
||||
<!-- [Status from the options: proposed, accepted, rejected, deprecated, superseded] -->
|
||||
- **Status**
|
||||
|
||||
## Context
|
||||
|
||||
<!-- [Description of the context and problem statement that the decision is addressing. It should contain any relevant factors that influenced the decision.] -->
|
||||
|
||||
## Decision
|
||||
|
||||
<!-- [Description of the decision that was made. Detail the change that will be implemented.] -->
|
||||
|
||||
## Consequences
|
||||
|
||||
<!-- [Explanation of the consequences of the decision. This includes both the positive and negative effects, and any potential risks.] -->
|
||||
|
||||
### Positive Consequences
|
||||
|
||||
- consequence 1
|
||||
- consequence 2
|
||||
|
||||
### Negative Consequences
|
||||
|
||||
- consequence 1
|
||||
- consequence 2
|
||||
|
||||
32
docs/architecture/adr/0001-switch-from-java-to-kotlin.md
Normal file
32
docs/architecture/adr/0001-switch-from-java-to-kotlin.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# Switch from Java to Kotlin
|
||||
|
||||
- Pull Request: [#7221](https://github.com/thunderbird/thunderbird-android/pull/7221)
|
||||
|
||||
## Status
|
||||
|
||||
- **Accepted**
|
||||
|
||||
## Context
|
||||
|
||||
We've been using Java as our primary language for Android development. While Java has served us well, it has certain
|
||||
limitations in terms of null safety, verbosity, functional programming, and more. Kotlin, officially supported by
|
||||
Google for Android development, offers solutions to many of these issues and provides more modern language features
|
||||
that can improve productivity, maintainability, and overall code quality.
|
||||
|
||||
## Decision
|
||||
|
||||
Switch our primary programming language for Android development from Java to Kotlin. This will involve rewriting our
|
||||
existing Java codebase in Kotlin and writing all new code in Kotlin. To facilitate the transition, we will gradually
|
||||
refactor our existing Java codebase to Kotlin.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **Positive Consequences**
|
||||
- Improved null safety, reducing potential for null pointer exceptions.
|
||||
- Increased code readability and maintainability due to less verbose syntax.
|
||||
- Availability of modern language features such as coroutines for asynchronous programming, and extension functions.
|
||||
- Officially supported by Google for Android development, ensuring future-proof development.
|
||||
- **Negative Consequences**
|
||||
- The process of refactoring existing Java code to Kotlin can be time-consuming.
|
||||
- Potential for introduction of new bugs during refactoring.
|
||||
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
# UI - Wrap Material Components in Atomic Design System
|
||||
|
||||
- Pull Request: [#7221](https://github.com/thunderbird/thunderbird-android/pull/7221)
|
||||
|
||||
## Status
|
||||
|
||||
- **Accepted**
|
||||
|
||||
## Context
|
||||
|
||||
As we continued developing our Jetpack Compose application, we found a need to increase the consistency, reusability,
|
||||
and maintainability of our user interface (UI) components. We have been using Material components directly throughout our
|
||||
application. This lead to a lack of uniformity and increases the complexity of changes as the same modifications had to
|
||||
be implemented multiple times across different screens.
|
||||
|
||||
## Decision
|
||||
|
||||
To address these challenges, we've decided to adopt an
|
||||
[Atomic Design System](../design-system.md) as a foundation for our application UI.
|
||||
This system encapsulates Material components within our [own components](../../../core/ui/compose/designsystem/),
|
||||
organized into categories of _atoms_, _molecules_, and _organisms_. We also defined _templates_ as layout structures
|
||||
that can be flexibly combined to construct _pages_. These components collectively form the building blocks that we are
|
||||
using to construct our application's UI.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **Positive Consequences**
|
||||
- Increased reusability of components across the application, reducing code duplication.
|
||||
- More consistent UI and uniform styling across the entire application.
|
||||
- Improved maintainability, as changes to a component only need to be made in one place.
|
||||
- **Negative Consequences**
|
||||
- Initial effort and time investment needed to implement the atomic design system.
|
||||
- Developers need to adapt to the new system and learn how to use it effectively.
|
||||
- Potential for over-complication if simple components are excessively broken down into atomic parts.
|
||||
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
# Switch Test Assertions from Truth to assertk
|
||||
|
||||
- Pull Request: [#7242](https://github.com/thunderbird/thunderbird-android/pull/7242)
|
||||
|
||||
## Status
|
||||
|
||||
- **Accepted**
|
||||
|
||||
## Context
|
||||
|
||||
Our project has been using the Truth testing library for writing tests. While Truth has served us well, it is primarily
|
||||
designed for Java and lacks some features that make our Kotlin tests more idiomatic and expressive. As our codebase is
|
||||
[primarily Kotlin](0001-switch-from-java-to-kotlin.md), we have been looking for a testing library that is more aligned
|
||||
with Kotlin's features and idioms.
|
||||
|
||||
## Decision
|
||||
|
||||
We have decided to use [assertk](https://github.com/willowtreeapps/assertk) as the default assertions framework for
|
||||
writing tests in our project. assertk provides a fluent API that is very similar to Truth, making the transition easier.
|
||||
Moreover, it is designed to work well with Kotlin, enabling us to leverage Kotlin-specific features in our tests.
|
||||
|
||||
We've further committed to converting all pre-existing tests from Truth to assertk.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Note**: The migration of all Truth tests to assertk has already been completed.
|
||||
|
||||
- **Positive Consequences**
|
||||
- **Ease of Transition**: The syntax of assertk is very similar to Truth, which makes the migration process smoother.
|
||||
- **Kotlin-Friendly**: assertk is designed specifically for Kotlin, allowing us to write more idiomatic and
|
||||
expressive Kotlin tests.
|
||||
- **Negative Consequences**
|
||||
- **Dependency**: While we are replacing one library with another, introducing a new library always carries the risk
|
||||
of bugs or future deprecation.
|
||||
- **Migration Effort**: Existing tests written using Truth will need to be migrated to use assertk, requiring some
|
||||
effort, although mitigated by the similar syntax.
|
||||
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
# Naming Conventions for Interfaces and Their Implementations
|
||||
|
||||
- Pull Request: [#7794](https://github.com/thunderbird/thunderbird-android/pull/7794)
|
||||
|
||||
## Status
|
||||
|
||||
- **Accepted**
|
||||
|
||||
## Context
|
||||
|
||||
When there's an interface that has multiple implementations it's often easy enough to give meaningful names to both the
|
||||
interface and the implementations (e.g. the interface `Backend` with the implementations `ImapBackend` and
|
||||
`Pop3Backend`). Naming becomes harder when the interface mainly exists to allow having isolated unit tests and the
|
||||
production code contains exactly one implementation of the interface.
|
||||
Prior to this ADR we didn't have any naming guidelines and the names varied widely. Often when there was only one
|
||||
(production) implementation, the class name used one of the prefixes `Default`, `Real`, or `K9`. None of these had any
|
||||
special meaning and it wasn't clear which one to pick when creating a new interface/class pair.
|
||||
|
||||
## Decision
|
||||
|
||||
We'll be using the following guidelines for naming interfaces and their implementation classes:
|
||||
|
||||
1. **Interface Naming:** Name interfaces as if they were classes, using a clear and descriptive name. Avoid using the
|
||||
"IInterface" pattern.
|
||||
2. **Implementation Naming:** Use a prefix that clearly indicates the relationship between the interface and
|
||||
implementation, such as `DatabaseMessageStore` or `InMemoryMessageStore` for the `MessageStore` interface.
|
||||
3. **Descriptive Names:** Use descriptive names for interfaces and implementing classes that accurately reflect their
|
||||
purpose and functionality.
|
||||
4. **Platform-specific Implementations:** Use the platform name as a prefix for interface implementations specific to
|
||||
that platform, e.g. `AndroidPowerManager`.
|
||||
5. **App-specific Implementations:** Use the prefix `K9` for K-9 Mail and `Tb` for Thunderbird when app-specific
|
||||
implementations are needed, e.g. `K9AppNameProvider` and `TbAppNameProvider`.
|
||||
6. **Flexibility:** If no brief descriptive name fits and there is only one production implementation, use the prefix
|
||||
`Default`, like `DefaultImapFolder`.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **Positive Consequences**
|
||||
- Improved code readability and maintainability through consistent naming.
|
||||
- Reduced confusion and misunderstandings by using clear and descriptive names.
|
||||
- **Negative Consequences**
|
||||
- Initial effort is required to rename existing classes that do not follow these naming conventions.
|
||||
|
||||
34
docs/architecture/adr/0005-central-project-configuration.md
Normal file
34
docs/architecture/adr/0005-central-project-configuration.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# Central Management of Android Project Dependencies and Gradle Configurations via Build-Plugin Module
|
||||
|
||||
- Pull Request: [#7803](https://github.com/thunderbird/thunderbird-android/pull/7803)
|
||||
|
||||
## Status
|
||||
|
||||
- **Accepted**
|
||||
|
||||
## Context
|
||||
|
||||
In our Android project, managing dependencies and configurations directly within each module's `build.gradle.kts` file has historically led to inconsistencies, duplication, and difficulty in updates. This challenge was particularly noticeable when maintaining the project configuration. By centralizing this setup in a `build-plugin` module, we can encapsulate and reuse Gradle logic, streamline the build process, and ensure consistency across all project modules and ease maintainability of our codebase.
|
||||
|
||||
## Decision
|
||||
|
||||
To address these challenges, we have decided to establish a `build-plugin` module within our project. This module will serve as the foundation for all common Gradle configurations, dependency management, and custom plugins, allowing for simplified configuration across various project modules and plugins. Key components of this module include:
|
||||
|
||||
- **Custom Plugins:** A suite of custom plugins that configure Gradle for different project aspects, ensuring each project type has tailored and streamlined build processes. These plugins should cover Android application, Android library, Jetpack Compose and Java modules.
|
||||
- **Dependency Management:** Utilizing the [Gradle Version Catalog](https://docs.gradle.org/current/userguide/platforms.html) to centrally manage and update all dependencies and plugins, ensuring that every module uses the same versions and reduces the risk of conflicts.
|
||||
- **Common Configuration Settings:** Establishing common configurations for Java, Kotlin, and Android to reduce the complexity and variability in setup across different modules.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive Consequences
|
||||
|
||||
1. **Consistency Across Modules:** All project modules will use the same versions of dependencies and plugins, reducing the risk of conflicts and enhancing uniformity. They will also share common configurations, ensuring consistency in the build process.
|
||||
2. **Ease of Maintenance:** Centralizing dependency versions in the Gradle Version Catalog allows for simple and quick updates to libraries and tools across all project modules from a single source.
|
||||
3. **Simplified Configuration Process:** The custom plugins within the `build-plugin` module provides a streamlined way to apply settings and dependencies uniformly, enhancing productivity and reducing setup complexity.
|
||||
|
||||
### Negative Consequences
|
||||
|
||||
1. **Initial Overhead:** The setup of the build-plugin module with a Gradle Version Catalog and the migration of existing configurations required an initial investment of time and resources, but this has been completed.
|
||||
2. **Complexity for New Developers:** The centralized build architecture, particularly with the use of a Gradle Version Catalog, may initially seem daunting to new team members who are unfamiliar with this level of abstraction.
|
||||
3. **Dependency on the Build-Plugin Module:** The entire project becomes reliant on the stability and accuracy of the `build-plugin` module. Errors within this module or the catalog could impact the build process across all modules.
|
||||
|
||||
37
docs/architecture/adr/0006-white-label-architecture.md
Normal file
37
docs/architecture/adr/0006-white-label-architecture.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# White Label Architecture
|
||||
|
||||
- Issue: [#7807](https://github.com/thunderbird/thunderbird-android/issues/7807)
|
||||
- Pull Request: [#7805](https://github.com/thunderbird/thunderbird-android/pull/7805)
|
||||
|
||||
## Status
|
||||
|
||||
- **Accepted**
|
||||
|
||||
## Context
|
||||
|
||||
Our project hosts two separate applications, K-9 Mail and Thunderbird for Android, which share a significant amount of functionality. Despite their common features, each app requires distinct branding elements such as app names, themes, and specific strings.
|
||||
|
||||
## Decision
|
||||
|
||||
We have decided to adopt a modular white-label architecture, where each application is developed as a separate module that relies on a shared codebase. This structure allows us to streamline configuration details specific to each brand either during build or at runtime. This is how we structure the modules:
|
||||
|
||||
### Application Modules
|
||||
|
||||
There will be 2 separate modules for each of the two applications: **Thunderbird for Android** will be located in `app-thunderbird` and **K-9 Mail** in `app-k9mail`. These modules will contain app-specific implementations, configurations, resources, and startup logic. They should solely depend on the `app-common` module for shared functionalities and may selectively integrate other modules when needed to configure app-specific functionality.
|
||||
|
||||
### App Common Module
|
||||
|
||||
A central module named `app-common` acts as the central integration point for shared code among the applications. This module contains the core functionality, shared resources, and configurations that are common to both apps. It should be kept as lean as possible to avoid unnecessary dependencies and ensure that it remains focused on shared functionality.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive Consequences
|
||||
|
||||
- Enhanced maintainability due to a shared codebase for common functionalities, reducing code duplication.
|
||||
- Increased agility in developing and deploying new features across both applications, as common enhancements need to be implemented only once.
|
||||
|
||||
## Negative Consequences
|
||||
|
||||
- Potential for configuration complexities as differentiations increase between the two applications.
|
||||
- Higher initial setup time and learning curve for new developers due to the modular and decoupled architecture.
|
||||
|
||||
158
docs/architecture/adr/0007-project-structure.md
Normal file
158
docs/architecture/adr/0007-project-structure.md
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
# Project Structure
|
||||
|
||||
- Issue: [#7852](https://github.com/thunderbird/thunderbird-android/issues/7852)
|
||||
- Pull Request: [#7829](https://github.com/thunderbird/thunderbird-android/pull/7829)
|
||||
|
||||
## Status
|
||||
|
||||
- **Accepted**
|
||||
|
||||
## Context
|
||||
|
||||
The project consists of two distinct applications. To improve maintainability and streamline development, we propose a modular structure using Gradle. This structure is designed to enable clear separation of concerns, facilitate scalable growth, and ensure efficient dependency management. It consists of various module types such as `app`, `app-common`, `feature`, `core`, and `library` modules, promoting enhanced modular reusability.
|
||||
|
||||
## Decision
|
||||
|
||||
To achieve the goals outlined in the context, we have decided to adopt the following modular structure:
|
||||
|
||||
1. **App Modules**:
|
||||
- `app-thunderbird` and `app-k9mail` are the modules for the two applications, Thunderbird for Android and K-9 Mail respectively. These modules will contain app-specific implementations, configurations, resources, and startup logic. They should solely depend on the `app-common` module for shared functionalities and may selectively integrate `feature` and `core` to setup app-specific needs.
|
||||
2. **App Common Module**:
|
||||
- `app-common`: Acts as the central hub for shared code between both applications. This module serves as the primary "glue" that binds various `feature` modules together, providing a seamless integration point. While it can depend on `library` modules for additional functionalities, its main purpose is to orchestrate the interactions among the `feature` and `core` modules, ensuring similar functionality across both applications. This module should be kept lean to avoid unnecessary dependencies and ensure it remains focused on shared functionality.
|
||||
3. **Feature Modules**:
|
||||
- `feature:*`: These are independent feature modules, that encapsulate distinct user-facing features. They are designed to be reusable and can be integrated into any application module as needed. They maintain dependencies on `core` modules and may interact with other `feature` or `library` modules.
|
||||
4. **Core Module**:
|
||||
- `core:*`: The core modules contain essential utilities and base classes used across the entire project. These modules are grouped by their functionality (e.g., networking, database management, theming, common utilities). This segmentation allows for cleaner dependency management and specialization within foundational aspects.
|
||||
5. **Library Modules**:
|
||||
- `library:*` These modules are for specific implementations that might be used across various features or applications. They could be third-party integrations or complex utilities and eventually shared across multiple projects.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph APP[App]
|
||||
APP_K9["`
|
||||
**:app-k9mail**
|
||||
K-9 Mail
|
||||
`"]
|
||||
APP_TB["`
|
||||
**:app-thunderbird**
|
||||
Thunderbird for Android
|
||||
`"]
|
||||
end
|
||||
|
||||
subgraph COMMON[App Common]
|
||||
APP_COMMON["`
|
||||
**:app-common**
|
||||
Integration Code
|
||||
`"]
|
||||
end
|
||||
|
||||
subgraph FEATURE[Feature]
|
||||
FEATURE1[Feature 1]
|
||||
FEATURE2[Feature 2]
|
||||
end
|
||||
|
||||
subgraph CORE[Core]
|
||||
CORE1[Core 1]
|
||||
CORE2[Core 2]
|
||||
end
|
||||
|
||||
subgraph LIBRARY[Library]
|
||||
LIB1[Library 1]
|
||||
LIB2[Library 2]
|
||||
end
|
||||
|
||||
APP --> |depends on| COMMON
|
||||
COMMON --> |integrates| FEATURE
|
||||
FEATURE --> |uses| CORE
|
||||
FEATURE --> |uses| LIBRARY
|
||||
|
||||
classDef module fill:yellow
|
||||
classDef app fill:azure
|
||||
classDef app_common fill:#ddd
|
||||
class APP_K9 app
|
||||
class APP_TB app
|
||||
class APP_COMMON app_common
|
||||
```
|
||||
|
||||
### Legacy Modules
|
||||
|
||||
Modules that are still required for the project to function, but don't follow the new project structure.
|
||||
|
||||
These modules should not be used for new development.
|
||||
|
||||
The goal is to migrate the functionality of these modules to the new structure over time. By placing them under the `legacy` module, we can easily identify and manage them.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph APP[App]
|
||||
APP_K9["`
|
||||
**:app-k9mail**
|
||||
K-9 Mail
|
||||
`"]
|
||||
APP_TB["`
|
||||
**:app-thunderbird**
|
||||
Thunderbird for Android
|
||||
`"]
|
||||
end
|
||||
|
||||
subgraph COMMON[App Common]
|
||||
APP_COMMON["`
|
||||
**:app-common**
|
||||
Integration Code
|
||||
`"]
|
||||
end
|
||||
|
||||
subgraph FEATURE[Feature]
|
||||
FEATURE1[Feature 1]
|
||||
FEATURE2[Feature 2]
|
||||
FEATURE3[Feature from Legacy]
|
||||
end
|
||||
|
||||
subgraph CORE[Core]
|
||||
CORE1[Core 1]
|
||||
CORE2[Core 2]
|
||||
CORE3[Core from Legacy]
|
||||
end
|
||||
|
||||
subgraph LIBRARY[Library]
|
||||
LIB1[Library 1]
|
||||
LIB2[Library 2]
|
||||
end
|
||||
|
||||
APP --> |depends on| COMMON
|
||||
COMMON --> |integrates| FEATURE
|
||||
FEATURE --> |uses| CORE
|
||||
FEATURE --> |uses| LIBRARY
|
||||
|
||||
subgraph LEGACY[Legacy]
|
||||
LEG[Legacy Code]
|
||||
end
|
||||
|
||||
COMMON -.-> |integrates| LEGACY
|
||||
LEG -.-> |migrate to| FEATURE3
|
||||
LEG -.-> |migrate to| CORE3
|
||||
|
||||
classDef module fill:yellow
|
||||
classDef app fill:azure
|
||||
classDef app_common fill:#ddd
|
||||
classDef legacy fill:#F99
|
||||
class APP_K9 app
|
||||
class APP_TB app
|
||||
class APP_COMMON app_common
|
||||
class LEGACY legacy
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive Consequences
|
||||
|
||||
- Improved modularity facilitates easier code maintenance and scaling.
|
||||
- Clear separation of concerns reduces dependencies and potential conflicts between modules.
|
||||
- Enhanced reusability of the `feature`, `core` and `library` modules across different parts of the application or even in different projects.
|
||||
|
||||
### Negative Consequences
|
||||
|
||||
- Initial complexity in setting up and managing multiple modules may increase the learning curve and setup time for new developers.
|
||||
- Over-modularization can lead to excessive abstraction, potentially impacting runtime performance and complicating the debugging process.
|
||||
- Legacy modules may require additional effort to migrate to the new structure, potentially causing delays in the adoption of the new architecture.
|
||||
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
# Change Shared Modules package to `net.thunderbird`
|
||||
|
||||
- Issue: [#9012](https://github.com/thunderbird/thunderbird-android/issues/9012)
|
||||
|
||||
## Status
|
||||
|
||||
- **Accepted**
|
||||
|
||||
## Context
|
||||
|
||||
The Thunderbird Android project is a white-label version of K-9 Mail, and both apps — `app-thunderbird` and `app-kmail`
|
||||
— coexist in the same repository. They have distinct application IDs and branding, but share a significant portion of
|
||||
the code through common modules.
|
||||
|
||||
These shared modules currently use the `app.k9mail` or `com.fsck` package name, which are legacy artifacts from
|
||||
K-9 Mail. While K-9 will remain available for some time, the project’s primary focus has shifted toward Thunderbird.
|
||||
|
||||
To reflect this shift, establish clearer ownership, and prepare for future development (including cross-platform code
|
||||
integration), we will rename the packages in shared modules from `app.k9mail` and `com.fsck` to `net.thunderbird`.
|
||||
The actual application IDs and package names of `app-thunderbird` and `app-k9mail` must remain **unchanged**.
|
||||
|
||||
## Decision
|
||||
|
||||
We decided to rename the base package in all shared modules from `app.k9mail` and `com.fsck` to `net.thunderbird`.
|
||||
|
||||
Specifically:
|
||||
|
||||
- All Kotlin/Java packages in shared modules will be refactored to use `net.thunderbird` as the base
|
||||
- This must not affect the application IDs or packages of `app-thunderbird` or `app-kmail`, which will remain as-is
|
||||
- All references, imports, and configuration references will be updated accordingly
|
||||
- Tests, resources, and Gradle module settings will be adjusted to match the new package structure
|
||||
|
||||
This change will establish a clearer identity for the shared code, align with Thunderbird's branding, and prepare the
|
||||
project for cross-platform development.
|
||||
|
||||
## Consequences
|
||||
|
||||
## Positive Consequences
|
||||
|
||||
- Shared code reflects Thunderbird branding and identity
|
||||
- Reduces confusion when navigating codebase shared by both apps
|
||||
- Sets the foundation for cross-platform compatibility and future modularization
|
||||
- Helps reinforce long-term direction of the project toward Thunderbird
|
||||
|
||||
## Negative Consequences
|
||||
|
||||
- Large-scale refactoring required across multiple modules
|
||||
- Risk of introducing regressions during package renaming
|
||||
- Potential for disruption in local development setups (e.g., IDE caching, broken imports)
|
||||
- Contributors familiar with the old structure may need time to adjust
|
||||
|
||||
83
docs/architecture/adr/README.md
Normal file
83
docs/architecture/adr/README.md
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# Architecture Decision Records
|
||||
|
||||
The [docs/architecture/adr](/docs/architecture/adr) folder contains the architecture decision records (ADRs) for our project.
|
||||
|
||||
ADRs are short text documents that serve as a historical context for the architecture decisions we make over the
|
||||
course of the project.
|
||||
|
||||
## What is an ADR?
|
||||
|
||||
An Architecture Decision Record (ADR) is a document that captures an important architectural decision made along
|
||||
with its context and consequences. ADRs record the decision making process and allow others to understand the
|
||||
rationale behind decisions, providing insight and facilitating future decision-making processes.
|
||||
|
||||
## Format of an ADR
|
||||
|
||||
We adhere to Michael Nygard's [ADR format proposal](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions),
|
||||
where each ADR document should contain:
|
||||
|
||||
1. **Title**: A short descriptive name for the decision.
|
||||
1. **Link to Issue**: A link to the issue that prompted the decision.
|
||||
2. **Link to Pull Request**: A link to the pull request that implements the ADR.
|
||||
3. **Link to Tracking Issue**: A link to the tracking issue, if applicable.
|
||||
2. **Status**: The current status of the decision (proposed, accepted, rejected, deprecated, superseded)
|
||||
3. **Context**: The context that motivates this decision.
|
||||
4. **Decision**: The change that we're proposing and/or doing.
|
||||
5. **Consequences**: What becomes easier or more difficult to do and any risks introduced as a result of the decision.
|
||||
|
||||
## Creating a new ADR
|
||||
|
||||
When creating a new ADR, please follow the provided [ADR template file](0000-adr-template.md) and ensure that your
|
||||
document is clear and concise.
|
||||
|
||||
Once you are ready to propose your ADR, you should:
|
||||
|
||||
1. Create an issue in the repository, get consensus from at least one other project contributor.
|
||||
2. Make a post on [the mobile-planning list](https://thunderbird.topicbox.com/groups/mobile-planning)
|
||||
to announce your ADR. You can use the below template as needed.
|
||||
3. Create a pull request in the repository linking the issue.
|
||||
4. Make a decision together with mobile module owners, the PR will be merged when accepted.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
The ADRs will be stored in a directory named `docs/adr`, and each ADR will be a file named `NNNN-title-with-dashes.md`
|
||||
where `NNNN` is a four-digit number that is increased by 1 for every new adr.
|
||||
|
||||
## ADR Life Cycle
|
||||
|
||||
The life cycle of an ADR is as follows:
|
||||
|
||||
1. **Proposed**: The ADR is under consideration.
|
||||
2. **Accepted**: The decision described in the ADR has been accepted and should be adhered to, unless it is superseded by another ADR.
|
||||
3. **Rejected**: The decision described in the ADR has been rejected.
|
||||
4. **Deprecated**: The decision described in the ADR is no longer relevant due to changes in system context.
|
||||
5. **Superseded**: The decision described in the ADR has been replaced by another decision.
|
||||
|
||||
Each ADR will have a status indicating its current life-cycle stage. An ADR can be updated over time, either to change
|
||||
the status or to add more information.
|
||||
|
||||
## Contributions
|
||||
|
||||
We welcome contributions in the form of new ADRs or updates to existing ones. Please ensure all contributions follow
|
||||
the standard format and provide clear and concise information.
|
||||
|
||||
## Appendix: Intent to Adopt Template
|
||||
|
||||
You may use this template in your Intent to Adopt email as noted above. Tweak it as you feel is useful.
|
||||
|
||||
> Hello everyone,
|
||||
>
|
||||
> I’m writing to share an intent to adopt a new architecture decision: [ADR-[Number]] [Title of ADR]
|
||||
>
|
||||
> This change addresses [brief summary of the problem] and proposes [brief description of the approach].
|
||||
>
|
||||
> This decision is based on [briefly mention motivating factors, constraints, or technical context].
|
||||
>
|
||||
> You can read the full proposal here: [link to ADR]
|
||||
>
|
||||
> If you have feedback or concerns, please respond in the linked issue. We plan to finalize the
|
||||
> decision after [proposed date], factoring in discussion at that time.
|
||||
>
|
||||
> Thanks,
|
||||
> [Your Name]
|
||||
|
||||
4
docs/architecture/assets/atomic_design.svg
Normal file
4
docs/architecture/assets/atomic_design.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 17 KiB |
39
docs/architecture/design-system.md
Normal file
39
docs/architecture/design-system.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# 🎨 Design System
|
||||
|
||||
The design system is a collection of guidelines, principles, and tools that help teams create consistent and cohesive visual designs and user experiences. It is built using the Atomic Design Methodology.
|
||||
|
||||
## 📚 Background
|
||||
|
||||
[Jetpack Compose](https://developer.android.com/jetpack/compose) is a declarative UI toolkit for Android that provides a modern and efficient way to build UIs for Android apps. In this context, design systems and atomic design can help designers and developers create more scalable, maintainable, and reusable UIs.
|
||||
|
||||
### 🧩 Design System
|
||||
|
||||
A design system is a collection of guidelines, principles, and tools that help teams create consistent and cohesive visual designs and user experiences.
|
||||
It typically includes a set of reusable components, such as icons, typography, color palettes, and layouts, that can be combined and customized to create new designs.
|
||||
|
||||
The design system also provides documentation and resources for designers and developers to ensure that the designs are implemented consistently and efficiently across all platforms and devices.
|
||||
The goal of a design system is to streamline the design process, improve design quality, and maintain brand consistency.
|
||||
|
||||
An example is Google's [Material Design](https://m3.material.io/) that is used to develop cohesive apps.
|
||||
|
||||
### 🧪 Atomic Design
|
||||
|
||||

|
||||
|
||||
Atomic design is a methodology for creating user interfaces (UI) in a design system by breaking them down into smaller, reusable components.
|
||||
These components are classified into five categories based on their level of abstraction: **atoms**, **molecules**, **organisms**, **templates**, and **pages**.
|
||||
|
||||
- **Atoms** are the smallest building blocks, such as buttons, labels, and input fields and could be combined to create more complex components.
|
||||
- **Molecules** are groups of atoms that work together, like search bars, forms or menus
|
||||
- **Organisms** are more complex components that combine molecules and atoms, such as headers or cards.
|
||||
- **Templates** are pages with placeholders for components
|
||||
- **Pages** are the final UI
|
||||
|
||||
By using atomic design, designers and developers can create more consistent and reusable UIs.
|
||||
This can save time and improve the overall quality, as well as facilitate collaboration between team members.
|
||||
|
||||
## 📝 Acknowledgement
|
||||
|
||||
- [Atomic Design Methodology | Atomic Design by Brad Frost](https://atomicdesign.bradfrost.com/chapter-2/)
|
||||
- [Atomic Design: Getting Started | Blog | We Are Mobile First](https://www.wearemobilefirst.com/blog/atomic-design)
|
||||
|
||||
487
docs/architecture/feature-modules.md
Normal file
487
docs/architecture/feature-modules.md
Normal file
|
|
@ -0,0 +1,487 @@
|
|||
# 📦 Feature Modules and Extensions
|
||||
|
||||
The Thunderbird for Android project is organized into multiple feature modules, each encapsulating a specific
|
||||
functionality of the application. This document provides an overview of the main feature modules, how they are
|
||||
split into subfeatures, and how the application can be extended with additional features.
|
||||
|
||||
## 📏 Feature Module Best Practices
|
||||
|
||||
When developing new feature modules or extending existing ones, follow these best practices:
|
||||
|
||||
1. **API-First Design**: Define clear public interfaces before implementation
|
||||
2. **Single Responsibility**: Each feature module should have a single, well-defined responsibility
|
||||
3. **Minimal Dependencies**: Minimize dependencies between feature modules
|
||||
4. **Proper Layering**: Follow Clean Architecture principles within each feature
|
||||
5. **Testability**: Design features to be easily testable in isolation
|
||||
6. **Documentation**: Document the purpose and usage of each feature module
|
||||
7. **Consistent Naming**: Follow the established naming conventions
|
||||
8. **Feature Flags**: Use feature flags for gradual rollout and A/B testing
|
||||
9. **Accessibility**: Ensure all features are accessible to all users
|
||||
10. **Internationalization**: Design features with internationalization in mind
|
||||
|
||||
By following these guidelines, the Thunderbird for Android application can maintain a clean, modular architecture while
|
||||
expanding its functionality to meet user needs.
|
||||
|
||||
## 📋 Feature Module Overview
|
||||
|
||||
The application is composed of several core feature modules, each responsible for a specific aspect of the
|
||||
application's functionality:
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph FEATURE[App Features]
|
||||
direction TB
|
||||
|
||||
|
||||
subgraph ROW_2[" "]
|
||||
direction LR
|
||||
SETTINGS["`**Settings**<br>App configuration`"]
|
||||
NOTIFICATION["`**Notification**<br>Push and alert handling`"]
|
||||
SEARCH["`**Search**<br>Content discovery`"]
|
||||
WIDGET["`**Widget**<br>Home screen components`"]
|
||||
end
|
||||
|
||||
subgraph ROW_1[" "]
|
||||
direction LR
|
||||
ACCOUNT["`**Account**<br>User accounts management`"]
|
||||
MAIL["`**Mail**<br>Email handling and display`"]
|
||||
NAVIGATION["`**Navigation**<br>App navigation and UI components`"]
|
||||
ONBOARDING["`**Onboarding**<br>User setup and introduction`"]
|
||||
end
|
||||
end
|
||||
|
||||
classDef row fill: #d9ffd9, stroke: #d9ffd9, color: #d9ffd9
|
||||
classDef feature fill: #d9ffd9,stroke: #000000, color: #000000
|
||||
classDef feature_module fill: #33cc33, stroke: #000000, color:#000000
|
||||
|
||||
class ROW_1,ROW_2 row
|
||||
class FEATURE feature
|
||||
class ACCOUNT,MAIL,NAVIGATION,ONBOARDING,SETTINGS,NOTIFICATION,SEARCH,WIDGET feature_module
|
||||
```
|
||||
|
||||
## 🧩 Feature Module Details
|
||||
|
||||
### 🔑 Account Module
|
||||
|
||||
The Account module manages all aspects of email accounts, including setup, configuration, and authentication.
|
||||
|
||||
```shell
|
||||
feature:account
|
||||
├── feature:account:api
|
||||
├── feature:account:impl
|
||||
├── feature:account:setup
|
||||
│ ├── feature:account:setup:api
|
||||
│ └── feature:account:setup:impl
|
||||
├── feature:account:settings
|
||||
│ ├── feature:account:settings:api
|
||||
│ └── feature:account:settings:impl
|
||||
├── feature:account:server
|
||||
│ ├── feature:account:server:api
|
||||
│ ├── feature:account:server:impl
|
||||
│ ├── feature:account:server:certificate
|
||||
│ │ ├── feature:account:server:certificate:api
|
||||
│ │ └── feature:account:server:certificate:impl
|
||||
│ ├── feature:account:server:settings
|
||||
│ │ ├── feature:account:server:settings:api
|
||||
│ │ └── feature:account:server:settings:impl
|
||||
│ └── feature:account:server:validation
|
||||
│ ├── feature:account:server:validation:api
|
||||
│ └── feature:account:server:validation:impl
|
||||
├── feature:account:auth
|
||||
│ ├── feature:account:auth:api
|
||||
│ ├── feature:account:auth:impl
|
||||
│ └── feature:account:auth:oauth
|
||||
│ ├── feature:account:auth:oauth:api
|
||||
│ └── feature:account:auth:oauth:impl
|
||||
└── feature:account:storage
|
||||
├── feature:account:storage:api
|
||||
├── feature:account:storage:impl
|
||||
└── feature:account:storage:legacy
|
||||
├── feature:account:storage:legacy:api
|
||||
└── feature:account:storage:legacy:impl
|
||||
```
|
||||
|
||||
#### Subfeatures:
|
||||
|
||||
- **API/Implementation**: Core public interfaces and implementations for account management
|
||||
- **Setup**: New account setup wizard functionality
|
||||
- **API**: Public interfaces for account setup
|
||||
- **Implementation**: Concrete implementations of setup flows
|
||||
- **Settings**: Account-specific settings management
|
||||
- **API**: Public interfaces for account settings
|
||||
- **Implementation**: Concrete implementations of settings functionality
|
||||
- **Server**: Server configuration and management
|
||||
- **API/Implementation**: Core server management interfaces and implementations
|
||||
- **Certificate**: SSL certificate handling
|
||||
- **Settings**: Server settings configuration
|
||||
- **Validation**: Server connection validation
|
||||
- **Auth**: Authentication functionality
|
||||
- **API/Implementation**: Core authentication interfaces and implementations
|
||||
- **OAuth**: OAuth-specific authentication implementation
|
||||
- **Storage**: Account data persistence
|
||||
- **API/Implementation**: Core storage interfaces and implementations
|
||||
- **Legacy**: Legacy storage implementation
|
||||
|
||||
### 📧 Mail Module
|
||||
|
||||
The Mail module handles core email functionality, including message display, composition, and folder management.
|
||||
|
||||
```shell
|
||||
feature:mail
|
||||
├── feature:mail:api
|
||||
├── feature:mail:impl
|
||||
├── feature:mail:account
|
||||
│ ├── feature:mail:account:api
|
||||
│ └── feature:mail:account:impl
|
||||
├── feature:mail:folder
|
||||
│ ├── feature:mail:folder:api
|
||||
│ └── feature:mail:folder:impl
|
||||
├── feature:mail:compose
|
||||
│ ├── feature:mail:compose:api
|
||||
│ └── feature:mail:compose:impl
|
||||
└── feature:mail:message
|
||||
├── feature:mail:message:api
|
||||
├── feature:mail:message:impl
|
||||
├── feature:mail:message:view
|
||||
│ ├── feature:mail:message:view:api
|
||||
│ └── feature:mail:message:view:impl
|
||||
└── feature:mail:message:list
|
||||
├── feature:mail:message:list:api
|
||||
└── feature:mail:message:list:impl
|
||||
```
|
||||
|
||||
#### Subfeatures:
|
||||
|
||||
- **API/Implementation**: Core public interfaces and implementations for mail functionality
|
||||
- **Account**: Mail-specific account interfaces and implementations
|
||||
- **API**: Public interfaces for mail account integration
|
||||
- **Implementation**: Concrete implementations of mail account functionality
|
||||
- **Folder**: Email folder management
|
||||
- **API**: Public interfaces for folder operations
|
||||
- **Implementation**: Concrete implementations of folder management
|
||||
- **Compose**: Email composition functionality
|
||||
- **API**: Public interfaces for message composition
|
||||
- **Implementation**: Concrete implementations of composition features
|
||||
- **Message**: Message handling and display
|
||||
- **API/Implementation**: Core message handling interfaces and implementations
|
||||
- **View**: Individual message viewing functionality
|
||||
- **List**: Message list display and interaction
|
||||
|
||||
### 🧭 Navigation Module
|
||||
|
||||
The Navigation module provides UI components for navigating through the application.
|
||||
|
||||
```shell
|
||||
feature:navigation
|
||||
├── feature:navigation:api
|
||||
├── feature:navigation:impl
|
||||
└── feature:navigation:drawer
|
||||
├── feature:navigation:drawer:api
|
||||
├── feature:navigation:drawer:impl
|
||||
├── feature:navigation:drawer:dropdown
|
||||
│ ├── feature:navigation:drawer:dropdown:api
|
||||
│ └── feature:navigation:drawer:dropdown:impl
|
||||
└── feature:navigation:drawer:siderail
|
||||
├── feature:navigation:drawer:siderail:api
|
||||
└── feature:navigation:drawer:siderail:impl
|
||||
```
|
||||
|
||||
#### Subfeatures:
|
||||
|
||||
- **API/Implementation**: Core public interfaces and implementations for navigation
|
||||
- **Drawer**: Navigation drawer functionality
|
||||
- **API/Implementation**: Core drawer interfaces and implementations
|
||||
- **Dropdown**: Dropdown-style navigation implementation
|
||||
- **Siderail**: Side rail navigation implementation
|
||||
|
||||
### 🚀 Onboarding Module
|
||||
|
||||
The Onboarding module guides new users through the initial setup process.
|
||||
|
||||
```shell
|
||||
feature:onboarding
|
||||
├── feature:onboarding:api
|
||||
├── feature:onboarding:impl
|
||||
├── feature:onboarding:main
|
||||
│ ├── feature:onboarding:main:api
|
||||
│ └── feature:onboarding:main:impl
|
||||
├── feature:onboarding:welcome
|
||||
│ ├── feature:onboarding:welcome:api
|
||||
│ └── feature:onboarding:welcome:impl
|
||||
├── feature:onboarding:permissions
|
||||
│ ├── feature:onboarding:permissions:api
|
||||
│ └── feature:onboarding:permissions:impl
|
||||
└── feature:onboarding:migration
|
||||
├── feature:onboarding:migration:api
|
||||
├── feature:onboarding:migration:impl
|
||||
├── feature:onboarding:migration:thunderbird
|
||||
│ ├── feature:onboarding:migration:thunderbird:api
|
||||
│ └── feature:onboarding:migration:thunderbird:impl
|
||||
└── feature:onboarding:migration:noop
|
||||
├── feature:onboarding:migration:noop:api
|
||||
└── feature:onboarding:migration:noop:impl
|
||||
```
|
||||
|
||||
#### Subfeatures:
|
||||
|
||||
- **API/Implementation**: Core public interfaces and implementations for onboarding
|
||||
- **Main**: Main onboarding flow
|
||||
- **API**: Public interfaces for the main onboarding process
|
||||
- **Implementation**: Concrete implementations of the onboarding flow
|
||||
- **Welcome**: Welcome screens and initial user experience
|
||||
- **API**: Public interfaces for welcome screens
|
||||
- **Implementation**: Concrete implementations of welcome screens
|
||||
- **Permissions**: Permission request handling
|
||||
- **API**: Public interfaces for permission management
|
||||
- **Implementation**: Concrete implementations of permission requests
|
||||
- **Migration**: Data migration from other apps
|
||||
- **API/Implementation**: Core migration interfaces and implementations
|
||||
- **Thunderbird**: Thunderbird-specific migration implementation
|
||||
- **Noop**: No-operation implementation for testing
|
||||
|
||||
### ⚙️ Settings Module
|
||||
|
||||
The Settings module provides interfaces for configuring application behavior.
|
||||
|
||||
```shell
|
||||
feature:settings
|
||||
├── feature:settings:api
|
||||
├── feature:settings:impl
|
||||
├── feature:settings:import
|
||||
│ ├── feature:settings:import:api
|
||||
│ └── feature:settings:import:impl
|
||||
└── feature:settings:ui
|
||||
├── feature:settings:ui:api
|
||||
└── feature:settings:ui:impl
|
||||
```
|
||||
|
||||
#### Subfeatures:
|
||||
|
||||
- **API/Implementation**: Core public interfaces and implementations for settings
|
||||
- **Import**: Settings import functionality
|
||||
- **API**: Public interfaces for settings import
|
||||
- **Implementation**: Concrete implementations of import functionality
|
||||
- **UI**: Settings user interface components
|
||||
- **API**: Public interfaces for settings UI
|
||||
- **Implementation**: Concrete implementations of settings screens
|
||||
|
||||
### 🔔 Notification Module
|
||||
|
||||
The Notification module handles push notifications and alerts for new emails and events.
|
||||
|
||||
```shell
|
||||
feature:notification
|
||||
├── feature:notification:api
|
||||
├── feature:notification:impl
|
||||
├── feature:notification:email
|
||||
│ ├── feature:notification:email:api
|
||||
│ └── feature:notification:email:impl
|
||||
└── feature:notification:push
|
||||
├── feature:notification:push:api
|
||||
└── feature:notification:push:impl
|
||||
```
|
||||
|
||||
#### Subfeatures:
|
||||
|
||||
- **API/Implementation**: Core public interfaces and implementations for notifications
|
||||
- **Email**: Email-specific notification handling
|
||||
- **API**: Public interfaces for email notifications
|
||||
- **Implementation**: Concrete implementations of email alerts
|
||||
- **Push**: Push notification handling
|
||||
- **API**: Public interfaces for push notifications
|
||||
- **Implementation**: Concrete implementations of push notification processing
|
||||
|
||||
### 🔍 Search Module
|
||||
|
||||
The Search module provides functionality for searching through emails and contacts.
|
||||
|
||||
```shell
|
||||
feature:search
|
||||
├── feature:search:api
|
||||
├── feature:search:impl
|
||||
├── feature:search:email
|
||||
│ ├── feature:search:email:api
|
||||
│ └── feature:search:email:impl
|
||||
├── feature:search:contact
|
||||
│ ├── feature:search:contact:api
|
||||
│ └── feature:search:contact:impl
|
||||
└── feature:search:ui
|
||||
├── feature:search:ui:api
|
||||
└── feature:search:ui:impl
|
||||
```
|
||||
|
||||
#### Subfeatures:
|
||||
|
||||
- **API/Implementation**: Core public interfaces and implementations for search functionality
|
||||
- **Email**: Email-specific search capabilities
|
||||
- **API**: Public interfaces for email search
|
||||
- **Implementation**: Concrete implementations of email search
|
||||
- **Contact**: Contact search functionality
|
||||
- **API**: Public interfaces for contact search
|
||||
- **Implementation**: Concrete implementations of contact search
|
||||
- **UI**: Search user interface components
|
||||
- **API**: Public interfaces for search UI
|
||||
- **Implementation**: Concrete implementations of search screens
|
||||
|
||||
### 📱 Widget Module
|
||||
|
||||
The Widget module provides home screen widgets for quick access to email functionality.
|
||||
|
||||
```shell
|
||||
feature:widget
|
||||
├── feature:widget:api
|
||||
├── feature:widget:impl
|
||||
├── feature:widget:message-list
|
||||
│ ├── feature:widget:message-list:api
|
||||
│ └── feature:widget:message-list:impl
|
||||
├── feature:widget:message-list-glance
|
||||
│ ├── feature:widget:message-list-glance:api
|
||||
│ └── feature:widget:message-list-glance:impl
|
||||
├── feature:widget:shortcut
|
||||
│ ├── feature:widget:shortcut:api
|
||||
│ └── feature:widget:shortcut:impl
|
||||
└── feature:widget:unread
|
||||
├── feature:widget:unread:api
|
||||
└── feature:widget:unread:impl
|
||||
```
|
||||
|
||||
#### Subfeatures:
|
||||
|
||||
- **API/Implementation**: Core public interfaces and implementations for widgets
|
||||
- **Message List**: Email list widget
|
||||
- **API**: Public interfaces for message list widget
|
||||
- **Implementation**: Concrete implementations of message list widget
|
||||
- **Message List Glance**: Glanceable message widget
|
||||
- **API**: Public interfaces for glance widget
|
||||
- **Implementation**: Concrete implementations of glance widget
|
||||
- **Shortcut**: App shortcut widgets
|
||||
- **API**: Public interfaces for shortcut widgets
|
||||
- **Implementation**: Concrete implementations of shortcut widgets
|
||||
- **Unread**: Unread message counter widget
|
||||
- **API**: Public interfaces for unread counter widget
|
||||
- **Implementation**: Concrete implementations of unread counter widget
|
||||
|
||||
## 🔄 Supporting Feature Modules
|
||||
|
||||
In addition to the core email functionality, the application includes several supporting feature modules:
|
||||
|
||||
### 🔎 Autodiscovery Module
|
||||
|
||||
The Autodiscovery module automatically detects email server settings.
|
||||
|
||||
#### Subfeatures:
|
||||
|
||||
- **API** (`feature:autodiscovery:api`): Public interfaces
|
||||
- **Autoconfig** (`feature:autodiscovery:autoconfig`): Automatic configuration
|
||||
- **Service** (`feature:autodiscovery:service`): Service implementation
|
||||
- **Demo** (`feature:autodiscovery:demo`): Demonstration implementation
|
||||
|
||||
### 💰 Funding Module
|
||||
|
||||
The Funding module handles in-app financial contributions and funding options.
|
||||
|
||||
#### Subfeatures:
|
||||
|
||||
- **API** (`feature:funding:api`): Public interfaces
|
||||
- **Google Play** (`feature:funding:googleplay`): Google Play billing integration
|
||||
- **Link** (`feature:funding:link`): External funding link handling
|
||||
- **Noop** (`feature:funding:noop`): No-operation implementation
|
||||
|
||||
### 🔄 Migration Module
|
||||
|
||||
The Migration module handles data migration between different email clients.
|
||||
|
||||
#### Subfeatures:
|
||||
|
||||
- **Provider** (`feature:migration:provider`): Migration data providers
|
||||
- **QR Code** (`feature:migration:qrcode`): QR code-based migration
|
||||
- **Launcher** (`feature:migration:launcher`): Migration launcher
|
||||
- **API** (`feature:migration:launcher:api`): Launcher interfaces
|
||||
- **Noop** (`feature:migration:launcher:noop`): No-operation implementation
|
||||
- **Thunderbird** (`feature:migration:launcher:thunderbird`): Thunderbird-specific implementation
|
||||
|
||||
### 📊 Telemetry Module
|
||||
|
||||
The Telemetry module handles usage analytics and reporting.
|
||||
|
||||
#### Subfeatures:
|
||||
|
||||
- **API** (`feature:telemetry:api`): Public interfaces
|
||||
- **Noop** (`feature:telemetry:noop`): No-operation implementation
|
||||
- **Glean** (`feature:telemetry:glean`): Mozilla Glean integration
|
||||
|
||||
## 🔌 Extending with Additional Features
|
||||
|
||||
The modular architecture of Thunderbird for Android allows for easy extension with additional features. To give you an
|
||||
idea how the app could be extended when building a new feature, here are some theoretical examples along with their
|
||||
structure:
|
||||
|
||||
### 📅 Calendar Feature
|
||||
|
||||
A Calendar feature could be added to integrate calendar functionality with email.
|
||||
|
||||
```shell
|
||||
feature:calendar
|
||||
├── feature:calendar:api
|
||||
├── feature:calendar:impl
|
||||
├── feature:calendar:event
|
||||
│ ├── feature:calendar:event:api
|
||||
│ └── feature:calendar:event:impl
|
||||
└── feature:calendar:sync
|
||||
├── feature:calendar:sync:api
|
||||
└── feature:calendar:sync:impl
|
||||
```
|
||||
|
||||
### 🗓️ Appointments Feature
|
||||
|
||||
An Appointments feature could manage meetings and appointments.
|
||||
|
||||
```shell
|
||||
feature:appointment
|
||||
├── feature:appointment:api
|
||||
├── feature:appointment:impl
|
||||
├── feature:appointment:scheduler
|
||||
│ ├── feature:appointment:scheduler:api
|
||||
│ └── feature:appointment:scheduler:impl
|
||||
└── feature:appointment:notification
|
||||
├── feature:appointment:notification:api
|
||||
└── feature:appointment:notification:impl
|
||||
```
|
||||
|
||||
## 🔗 Feature Relationships
|
||||
|
||||
Features in the application interact with each other through well-defined APIs. The diagram below illustrates the
|
||||
relationships between different features:
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph CORE[Core Features]
|
||||
ACCOUNT[Account]
|
||||
MAIL[Mail]
|
||||
end
|
||||
|
||||
subgraph EXTENSIONS[Potential Extensions]
|
||||
CALENDAR[Calendar]
|
||||
APPOINTMENT[Appointments]
|
||||
end
|
||||
|
||||
MAIL --> |uses| ACCOUNT
|
||||
|
||||
CALENDAR --> |integrates with| MAIL
|
||||
CALENDAR --> |uses| ACCOUNT
|
||||
APPOINTMENT --> |uses| ACCOUNT
|
||||
APPOINTMENT --> |integrates with| CALENDAR
|
||||
APPOINTMENT --> |uses| MAIL
|
||||
|
||||
linkStyle default stroke:#999,stroke-width:2px
|
||||
|
||||
classDef core fill:#e8c8ff,stroke:#000000,color:#000000
|
||||
classDef core_module fill:#c090ff,stroke:#000000,color:#000000
|
||||
classDef extension fill:#d0e0ff,stroke:#000000,color:#000000
|
||||
classDef extension_module fill:#8090ff,stroke:#000000,color:#000000
|
||||
class CORE core
|
||||
class ACCOUNT,MAIL,NAVIGATION,SETTINGS core_module
|
||||
class EXTENSIONS extension
|
||||
class CALENDAR,TODO,SYNC,NOTES,APPOINTMENT extension_module
|
||||
```
|
||||
|
||||
459
docs/architecture/legacy-module-integration.md
Normal file
459
docs/architecture/legacy-module-integration.md
Normal file
|
|
@ -0,0 +1,459 @@
|
|||
# 🔙 Legacy Module Integration
|
||||
|
||||
This document outlines how existing legacy code is integrated into the new modular architecture of the application and
|
||||
the strategy for its migration. The core principle is to isolate legacy code and provide a controlled way for
|
||||
newer modules to interact with legacy functionality without becoming directly dependent on it.
|
||||
|
||||
> [!NOTE]
|
||||
> This document should be read in conjunction with [Module Structure](module-structure.md) and [Module Organization](module-organization.md) to get a complete understanding of the modular architecture.
|
||||
|
||||
## Overview
|
||||
|
||||
The Thunderbird for Android project is transitioning from a monolithic architecture to a modular one. During this
|
||||
transition, we need to maintain compatibility with existing legacy code while gradually migrating to the new
|
||||
architecture. The `legacy:*`, `mail:*`, and `backend:*` modules contain functionality that is still essential for the
|
||||
project but does not yet adhere to the new modular architecture. These modules are integrated into the new architecture
|
||||
through the `:app-common` module, which acts as a bridge or adapter to provide access to legacy functionality without
|
||||
directly depending on it.
|
||||
|
||||
The key components in this integration strategy are:
|
||||
|
||||
1. **Legacy Modules**: `legacy:*`, `mail:*`, and `backend:*` modules containing existing functionality
|
||||
2. **Interfaces**: Well-defined interfaces in `feature:*:api` and `core:*` modules
|
||||
3. **App Common Bridge**: The `:app-common` module that implements these interfaces and delegates to legacy code
|
||||
4. **Dependency Injection**: Configuration that provides the appropriate implementations to modules
|
||||
|
||||
## Integration Approach "_The App Common Bridge_"
|
||||
|
||||
Newer application modules (such as features or core components) depend on well-defined **Interfaces**
|
||||
(e.g., those found in `feature:*:api` modules). Typically, a feature will provide its own modern **Implementation**
|
||||
(e.g., `:feature:mail:impl`) for its API.
|
||||
|
||||
However, to manage dependencies on code still within `legacy:*`, `mail:*`, and `backend:*` modules and prevent it
|
||||
from spreading, we use `app-common` as **bridge** or **adapter** to provide an alternative implementation for these. In
|
||||
this role, `app-common` is responsible for:
|
||||
|
||||
1. **Implementing interfaces**: `app-common` provides concrete implementations for interfaces that newer modules define.
|
||||
2. **Delegating to legacy code**: Internally, these `app-common` implementations will delegate calls, adapt data, and manage interactions with the underlying `legacy:*`, `mail:*`, and `backend:*` modules.
|
||||
3. **Containing glue code**: All logic required to connect the modern interfaces with the legacy systems is encapsulated within `app-common`.
|
||||
|
||||
This approach ensures that:
|
||||
* Newer modules are decoupled from legacy implementations: They only interact with the defined interfaces, regardless of whether the implementation is the modern feature `impl` or the `app-common` bridge.
|
||||
* Legacy code is isolated.
|
||||
* A clear path for refactoring is maintained: Initially, the application might be configured to use the `app-common` bridge. As new, native implementations in feature modules (e.g., `:feature:mail:impl`) mature, the dependency injection can be switched to use them, often without changes to the modules consuming the interface.
|
||||
|
||||
### Bridge Pattern Flow
|
||||
|
||||
The typical flow is:
|
||||
|
||||
1. **Interfaces**: Interfaces are defined, usually within the `api` module of a feature (e.g., `:feature:mail:api`) or a core module. These interfaces represent the contract for a piece of functionality.
|
||||
2. **New Module Dependency**: Newer modules (e.g., `:feature:somefeature:impl` or other parts of `:app-common`) depend on these defined interfaces, to avoid dependency on concrete legacy classes.
|
||||
3. **Implementation**: The `:app-common` module provides concrete implementations for these interfaces.
|
||||
4. **Delegation to Legacy**: Internally, these implementations within `:app-common` delegate the actual work to the code residing in the legacy modules (e.g., `legacy:*`, `mail:*`, `backend:*`).
|
||||
5. **Dependency Injection**: The application's dependency injection framework is configured to provide instances of these `:app-common` bridge implementations when a newer module requests an implementation of the interface.
|
||||
|
||||
This pattern ensures that newer modules remain decoupled from the specifics of legacy code.
|
||||
|
||||
The following diagram illustrates this pattern, showing how both a feature's own implementation and `app-common` can relate to the interfaces, with `app-common` specifically bridging to legacy systems:
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph FEATURE[Feature Modules]
|
||||
direction TB
|
||||
INTERFACES["`**Interfaces**<br> (e.g., :feature:mail:api)`"]
|
||||
IMPLEMENTATIONS["`**Implementations**<br> (e.g., :feature:mail:impl)`"]
|
||||
OTHER_MODULES["`**Other Modules**<br>(depend on Interfaces)`"]
|
||||
end
|
||||
|
||||
subgraph COMMON[App Common Module]
|
||||
direction TB
|
||||
COMMON_APP["`**:app-common**<br>Integration Code`"]
|
||||
end
|
||||
|
||||
subgraph LEGACY[Legacy Modules]
|
||||
direction TB
|
||||
LEGACY_K9["`**:legacy**`"]
|
||||
LEGACY_MAIL["`**:mail**`"]
|
||||
LEGACY_BACKEND["`**:backend**`"]
|
||||
end
|
||||
|
||||
OTHER_MODULES --> |uses| INTERFACES
|
||||
IMPLEMENTATIONS --> |depends on| INTERFACES
|
||||
COMMON_APP --> |implements| INTERFACES
|
||||
COMMON_APP --> |delegates to / wraps| LEGACY_K9
|
||||
COMMON_APP --> |delegates to / wraps| LEGACY_MAIL
|
||||
COMMON_APP --> |delegates to / wraps| LEGACY_BACKEND
|
||||
|
||||
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 legacy fill:#ffe6e6,stroke:#000000,color:#000000
|
||||
classDef legacy_module fill:#ff9999,stroke:#000000,color:#000000
|
||||
|
||||
linkStyle default stroke:#999,stroke-width:2px
|
||||
|
||||
class COMMON common
|
||||
class COMMON_APP common_module
|
||||
class FEATURE feature
|
||||
class INTERFACES,IMPLEMENTATIONS,OTHER_MODULES feature_module
|
||||
class LEGACY legacy
|
||||
class LEGACY_MAIL,LEGACY_BACKEND,LEGACY_K9 legacy_module
|
||||
```
|
||||
|
||||
### Implementation Techniques
|
||||
|
||||
Several techniques are used to implement the bridge pattern effectively:
|
||||
|
||||
1. **Wrapper Classes**: Creating immutable data classes that wrap legacy data structures, implementing interfaces from the new architecture. These wrappers should not contain conversion methods but should delegate this responsibility to specific mapper classes.
|
||||
|
||||
2. **Adapter Implementations**: Classes in `:app-common` that implement interfaces from the new architecture but delegate to legacy code internally.
|
||||
|
||||
3. **Data Conversion**: Dedicated mapper classes that handle mapping between legacy and new data structures, ensuring clean separation of concerns.
|
||||
|
||||
#### Example: Account Profile Bridge
|
||||
|
||||
A concrete example of this pattern is the account profile bridge, which demonstrates a complete implementation of the bridge pattern across multiple layers:
|
||||
|
||||
1. **Modern Interfaces**:
|
||||
- `AccountProfileRepository` in `feature:account:api` defines the high-level contract for account profile management
|
||||
- `AccountProfileLocalDataSource` in `feature:account:core` defines the data access contract
|
||||
2. **Modern Data Structure**: `AccountProfile` in `feature:account:api` is a clean, immutable data class that represents account profile information in the new architecture.
|
||||
3. **Repository Implementation**: `DefaultAccountProfileRepository` in `feature:account:core` implements the `AccountProfileRepository` interface and depends on `AccountProfileLocalDataSource`.
|
||||
4. **Bridge Implementation**: `DefaultAccountProfileLocalDataSource` in `app-common` implements the `AccountProfileLocalDataSource` interface and serves as the bridge to legacy code.
|
||||
5. **Legacy Access**: The bridge uses `DefaultLegacyAccountWrapperManager` to access legacy account data:
|
||||
- `LegacyAccountWrapperManager` in `core:android:account` defines the contract for legacy account access
|
||||
- `LegacyAccountWrapper` in `core:android:account` is an immutable wrapper around the legacy `LegacyAccount` class
|
||||
6. **Data Conversion**: The bridge uses a dedicated mapper class to convert between modern `AccountProfile` objects and legacy account data.
|
||||
7. **Dependency Injection**: The `appCommonAccountModule` in `app-common` registers `DefaultAccountProfileLocalDataSource` as implementations of the respective interface.
|
||||
|
||||
This multi-layered approach allows newer modules to interact with legacy account functionality through clean, modern interfaces without directly depending on legacy code. It also demonstrates how bridges can be composed, with higher-level bridges (AccountProfile) building on lower-level bridges (LegacyAccountWrapper).
|
||||
|
||||
## Testing Considerations
|
||||
|
||||
Testing bridge implementations requires special attention to ensure both the bridge itself and its integration with legacy code work correctly:
|
||||
|
||||
1. **Unit Testing Bridge Classes**:
|
||||
- Test the bridge implementation in isolation by faking/stubbing the legacy dependencies
|
||||
- Verify that the bridge correctly translates between the new interfaces and legacy code
|
||||
- Focus on testing the conversion logic and error handling
|
||||
2. **Integration Testing**:
|
||||
- Test the bridge with actual legacy code to ensure proper integration
|
||||
- Verify that the bridge correctly handles all edge cases from legacy code
|
||||
3. **Test Doubles**:
|
||||
- Create fake implementations of bridge classes for testing other components
|
||||
- Example: `FakeLegacyAccountWrapperManager` can be used to test components that depend on `LegacyAccountWrapperManager`
|
||||
4. **Migration Testing**:
|
||||
- When migrating from a legacy bridge to a new implementation, test both implementations with the same test suite
|
||||
- Ensure behavior consistency during the transition
|
||||
|
||||
### Testing Examples
|
||||
|
||||
Below are examples of tests for legacy module integration, demonstrating different testing approaches and best practices.
|
||||
|
||||
#### Example 1: Unit Testing a Bridge Implementation
|
||||
|
||||
This example shows how to test a bridge implementation (`DefaultAccountProfileLocalDataSource`) in isolation by using a fake implementation of the legacy dependency (`FakeLegacyAccountWrapperManager`):
|
||||
|
||||
```kotlin
|
||||
class DefaultAccountProfileLocalDataSourceTest {
|
||||
|
||||
@Test
|
||||
fun `getById should return account profile`() = runTest {
|
||||
// arrange
|
||||
val accountId = AccountIdFactory.create()
|
||||
val legacyAccount = createLegacyAccount(accountId)
|
||||
val accountProfile = createAccountProfile(accountId)
|
||||
val testSubject = createTestSubject(legacyAccount)
|
||||
|
||||
// act & assert
|
||||
testSubject.getById(accountId).test {
|
||||
assertThat(awaitItem()).isEqualTo(accountProfile)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getById should return null when account is not found`() = runTest {
|
||||
// arrange
|
||||
val accountId = AccountIdFactory.create()
|
||||
val testSubject = createTestSubject(null)
|
||||
|
||||
// act & assert
|
||||
testSubject.getById(accountId).test {
|
||||
assertThat(awaitItem()).isEqualTo(null)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `update should save account profile`() = runTest {
|
||||
// arrange
|
||||
val accountId = AccountIdFactory.create()
|
||||
val legacyAccount = createLegacyAccount(accountId)
|
||||
val accountProfile = createAccountProfile(accountId)
|
||||
|
||||
val updatedName = "updatedName"
|
||||
val updatedAccountProfile = accountProfile.copy(name = updatedName)
|
||||
|
||||
val testSubject = createTestSubject(legacyAccount)
|
||||
|
||||
// act & assert
|
||||
testSubject.getById(accountId).test {
|
||||
assertThat(awaitItem()).isEqualTo(accountProfile)
|
||||
|
||||
testSubject.update(updatedAccountProfile)
|
||||
|
||||
assertThat(awaitItem()).isEqualTo(updatedAccountProfile)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createTestSubject(
|
||||
legacyAccount: LegacyAccountWrapper?,
|
||||
): DefaultAccountProfileLocalDataSource {
|
||||
return DefaultAccountProfileLocalDataSource(
|
||||
accountManager = FakeLegacyAccountWrapperManager(
|
||||
initialAccounts = if (legacyAccount != null) {
|
||||
listOf(legacyAccount)
|
||||
} else {
|
||||
emptyList()
|
||||
},
|
||||
),
|
||||
dataMapper = DefaultAccountProfileDataMapper(
|
||||
avatarMapper = DefaultAccountAvatarDataMapper(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Key points:
|
||||
- The test creates a controlled test environment using a fake implementation of the legacy dependency
|
||||
- It tests both success cases and error handling (account not found)
|
||||
- It verifies that the bridge correctly translates between legacy data structures and domain models
|
||||
- The test is structured with clear arrange, act, and assert sections
|
||||
|
||||
#### Example 2: Creating Test Doubles for Legacy Dependencies
|
||||
|
||||
This example shows how to create a fake implementation of a legacy dependency (`FakeLegacyAccountWrapperManager`) for testing:
|
||||
|
||||
```kotlin
|
||||
internal class FakeLegacyAccountWrapperManager(
|
||||
initialAccounts: List<LegacyAccountWrapper> = emptyList(),
|
||||
) : LegacyAccountWrapperManager {
|
||||
|
||||
private val accountsState = MutableStateFlow(
|
||||
initialAccounts,
|
||||
)
|
||||
private val accounts: StateFlow<List<LegacyAccountWrapper>> = accountsState
|
||||
|
||||
override fun getAll(): Flow<List<LegacyAccountWrapper>> = accounts
|
||||
|
||||
override fun getById(id: AccountId): Flow<LegacyAccountWrapper?> = accounts
|
||||
.map { list ->
|
||||
list.find { it.id == id }
|
||||
}
|
||||
|
||||
override suspend fun update(account: LegacyAccountWrapper) {
|
||||
accountsState.update { currentList ->
|
||||
currentList.toMutableList().apply {
|
||||
removeIf { it.uuid == account.uuid }
|
||||
add(account)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Key points:
|
||||
- The fake implementation implements the same interface as the real implementation
|
||||
- It provides a simple in-memory implementation for testing
|
||||
- It uses Kotlin Flows to simulate the reactive behavior of the real implementation
|
||||
- It allows for easy setup of test data through the constructor parameter
|
||||
|
||||
#### Example 3: Testing Data Conversion Logic
|
||||
|
||||
This example shows how to test data conversion logic in bridge implementations:
|
||||
|
||||
```kotlin
|
||||
class DefaultAccountProfileDataMapperTest {
|
||||
|
||||
@Test
|
||||
fun `toDomain should convert ProfileDto to AccountProfile`() {
|
||||
// Arrange
|
||||
val dto = createProfileDto()
|
||||
val expected = createAccountProfile()
|
||||
|
||||
val testSubject = DefaultAccountProfileDataMapper(
|
||||
avatarMapper = FakeAccountAvatarDataMapper(
|
||||
dto = dto.avatar,
|
||||
domain = expected.avatar,
|
||||
),
|
||||
)
|
||||
|
||||
// Act
|
||||
val result = testSubject.toDomain(dto)
|
||||
|
||||
// Assert
|
||||
assertThat(result.id).isEqualTo(expected.id)
|
||||
assertThat(result.name).isEqualTo(expected.name)
|
||||
assertThat(result.color).isEqualTo(expected.color)
|
||||
assertThat(result.avatar).isEqualTo(expected.avatar)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toDto should convert AccountProfile to ProfileDto`() {
|
||||
// Arrange
|
||||
val domain = createAccountProfile()
|
||||
val expected = createProfileDto()
|
||||
|
||||
val testSubject = DefaultAccountProfileDataMapper(
|
||||
avatarMapper = FakeAccountAvatarDataMapper(
|
||||
dto = expected.avatar,
|
||||
domain = domain.avatar,
|
||||
),
|
||||
)
|
||||
|
||||
// Act
|
||||
val result = testSubject.toDto(domain)
|
||||
|
||||
// Assert
|
||||
assertThat(result.id).isEqualTo(expected.id)
|
||||
assertThat(result.name).isEqualTo(expected.name)
|
||||
assertThat(result.color).isEqualTo(expected.color)
|
||||
assertThat(result.avatar).isEqualTo(expected.avatar)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Key points:
|
||||
- The test verifies that the mapper correctly converts between legacy data structures (DTOs) and domain models
|
||||
- It tests both directions of the conversion (toDomain and toDto)
|
||||
- It uses a fake implementation of a dependency (FakeAccountAvatarDataMapper) to isolate the test
|
||||
- It verifies that all properties are correctly mapped
|
||||
|
||||
#### Best Practices for Testing Legacy Module Integration
|
||||
|
||||
1. **Isolate the Bridge**: Test the bridge implementation in isolation by using fake or mock implementations of legacy dependencies.
|
||||
2. **Test Both Directions**: For data conversion, test both directions (legacy to domain and domain to legacy).
|
||||
3. **Cover Edge Cases**: Test edge cases such as null values, empty collections, and error conditions.
|
||||
4. **Use Clear Test Structure**: Structure tests with clear arrange, act, and assert sections.
|
||||
5. **Create Reusable Test Fixtures**: Create helper methods for creating test data to make tests more readable and maintainable.
|
||||
6. **Test Reactive Behavior**: For reactive code (using Flows, LiveData, etc.), use appropriate testing utilities (e.g., Turbine for Flow testing).
|
||||
7. **Verify Integration**: In addition to unit tests, create integration tests that verify the bridge works correctly with actual legacy code.
|
||||
8. **Test Migration Path**: When migrating from a legacy bridge to a new implementation, test both implementations with the same test suite to ensure behavior consistency.
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
The long-term strategy involves gradually migrating functionality out of the legacy modules:
|
||||
|
||||
1. **Identify Functionality**: Pinpoint specific functionalities within legacy modules that need to be modernized.
|
||||
2. **Define Interfaces**: Ensure clear interfaces are defined (typically in feature `api` modules) for this functionality.
|
||||
3. **Entity Modeling**: Create proper domain entity models that represent the business objects as immutable data classes.
|
||||
4. **Implement in New Modules**: Re-implement the functionality within new, dedicated feature `impl` modules or core modules.
|
||||
5. **Update Bridge (Optional)**: If `:app-common` was bridging to this specific legacy code, its bridge implementation can be updated or removed.
|
||||
6. **Switch DI Configuration**: Update the dependency injection to provide the new modern implementation instead of the legacy bridge.
|
||||
7. **Retire Legacy Code**: Once no longer referenced, the corresponding legacy code can be safely removed.
|
||||
|
||||
### Migration Example
|
||||
|
||||
Using the account profile example, the migration process would look like:
|
||||
|
||||
1. **Identify**: Account profile functionality in legacy modules needs modernization.
|
||||
2. **Define Interfaces**:
|
||||
- `AccountProfileRepository` interface is defined in `feature:account:api`
|
||||
- `AccountProfileLocalDataSource` interface is defined in `feature:account:core`
|
||||
3. **Entity Modeling**: Create `AccountProfile` as an immutable data class in `feature:account:api`.
|
||||
4. **Implement**: Create a new implementation of `AccountProfileLocalDataSource` in a modern module, e.g., `feature:account:impl`.
|
||||
5. **Update Bridge**: Update or remove `DefaultAccountProfileLocalDataSource` in `app-common`.
|
||||
6. **Switch DI**: Update `appCommonAccountModule` to provide the new implementation instead of `DefaultAccountProfileLocalDataSource`.
|
||||
7. **Retire**: Once all references to legacy account code are removed, the legacy code and lower-level bridges (`LegacyAccountWrapperManager`, `DefaultLegacyAccountWrapperManager`) can be safely deleted.
|
||||
|
||||
This approach ensures a smooth transition with minimal disruption to the application's functionality.
|
||||
|
||||
## Dependency Direction
|
||||
|
||||
A strict dependency rule is enforced: **New modules (features, core) must not directly depend on legacy modules.**
|
||||
The dependency flow is always from newer modules to interfaces, with `:app-common` providing the implementation.
|
||||
If `:app-common` bridges to legacy code, that is an internal detail of `:app-common`.
|
||||
|
||||
The legacy module integration diagram below explains how legacy code is integrated into the new modular architecture:
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph APP[App Modules]
|
||||
direction TB
|
||||
APP_TB["`**:app-thunderbird**<br>Thunderbird for Android`"]
|
||||
APP_K9["`**:app-k9mail**<br>K-9 Mail`"]
|
||||
end
|
||||
|
||||
subgraph COMMON[App Common Module]
|
||||
direction TB
|
||||
COMMON_APP["`**:app-common**<br>Integration Code`"]
|
||||
end
|
||||
|
||||
subgraph FEATURE[Feature Modules]
|
||||
direction TB
|
||||
FEATURE1[Feature 1]
|
||||
FEATURE2[Feature 2]
|
||||
FEATURE3[Feature from Legacy]
|
||||
end
|
||||
|
||||
subgraph CORE[Core Modules]
|
||||
direction TB
|
||||
CORE1[Core 1]
|
||||
CORE2[Core 2]
|
||||
CORE3[Core from Legacy]
|
||||
end
|
||||
|
||||
subgraph LIBRARY[Library Modules]
|
||||
direction TB
|
||||
LIB1[Library 1]
|
||||
LIB2[Library 2]
|
||||
end
|
||||
|
||||
subgraph LEGACY[Legacy Modules]
|
||||
direction TB
|
||||
LEGACY_CODE[Legacy Code]
|
||||
end
|
||||
|
||||
APP_K9 --> |depends on| COMMON_APP
|
||||
APP_TB --> |depends on| COMMON_APP
|
||||
COMMON_APP --> |integrates| FEATURE1
|
||||
COMMON_APP --> |integrates| FEATURE2
|
||||
COMMON_APP --> |integrates| FEATURE3
|
||||
FEATURE1 --> |uses| CORE1
|
||||
FEATURE1 --> |uses| LIB2
|
||||
FEATURE2 --> |uses| CORE2
|
||||
FEATURE2 --> |uses| CORE3
|
||||
COMMON_APP --> |integrates| LEGACY_CODE
|
||||
LEGACY_CODE -.-> |migrate to| FEATURE3
|
||||
LEGACY_CODE -.-> |migrate to| CORE3
|
||||
|
||||
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 CORE core
|
||||
class CORE1,CORE2,CORE3 core_module
|
||||
class LIBRARY library
|
||||
class LIB1,LIB2 library_module
|
||||
class LEGACY legacy
|
||||
class LEGACY_CODE legacy_module
|
||||
```
|
||||
|
||||
310
docs/architecture/module-organization.md
Normal file
310
docs/architecture/module-organization.md
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
# 📦 Module Organization
|
||||
|
||||
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.
|
||||
|
||||
This document outlines the adopted module organization for the Thunderbird for Android project, serving as a guide for
|
||||
developers to understand the codebase structure and ensure consistent architectural patterns.
|
||||
|
||||
## 📂 Module Overview
|
||||
|
||||
The modules are organized into several types, each serving a specific purpose in the overall architecture:
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph APP[App Modules]
|
||||
direction TB
|
||||
APP_TB["`**:app-thunderbird**<br>Thunderbird for Android`"]
|
||||
APP_K9["`**:app-k9mail**<br>K-9 Mail`"]
|
||||
end
|
||||
|
||||
subgraph COMMON[App Common Module]
|
||||
direction TB
|
||||
COMMON_APP["`**:app-common**<br>Integration Code`"]
|
||||
end
|
||||
|
||||
subgraph FEATURE[Feature Modules]
|
||||
direction TB
|
||||
FEATURE_ACCOUNT["`**:feature:account**`"]
|
||||
FEATURE_SETTINGS["`**:feature:settings**`"]
|
||||
FEATURE_ONBOARDING["`**:feature:onboarding**`"]
|
||||
FEATURE_MAIL["`**:feature:mail**`"]
|
||||
end
|
||||
|
||||
subgraph CORE[Core Modules]
|
||||
direction TB
|
||||
CORE_UI["`**:core:ui**`"]
|
||||
CORE_COMMON["`**:core:common**`"]
|
||||
CORE_ANDROID["`**:core:android**`"]
|
||||
CORE_NETWORK["`**:core:network**`"]
|
||||
CORE_DATABASE["`**:core:database**`"]
|
||||
CORE_TESTING["`**:core:testing**`"]
|
||||
end
|
||||
|
||||
subgraph LIBRARY[Library Modules]
|
||||
direction TB
|
||||
LIB_AUTH["`**:library:auth**`"]
|
||||
LIB_CRYPTO["`**:library:crypto**`"]
|
||||
LIB_STORAGE["`**:library:storage**`"]
|
||||
end
|
||||
|
||||
subgraph LEGACY[Legacy Modules]
|
||||
direction TB
|
||||
LEGACY_K9["`**:legacy**`"]
|
||||
LEGACY_MAIL["`**:mail**`"]
|
||||
LEGACY_BACKEND["`**:backend**`"]
|
||||
end
|
||||
|
||||
APP ~~~ COMMON
|
||||
COMMON ~~~ FEATURE
|
||||
FEATURE ~~~ CORE
|
||||
CORE ~~~ LIBRARY
|
||||
LIBRARY ~~~ LEGACY
|
||||
|
||||
APP --> |depends on| COMMON
|
||||
COMMON --> |depends on| FEATURE
|
||||
FEATURE --> |depends on| CORE
|
||||
CORE --> |depends on| LIBRARY
|
||||
COMMON --> |depends on<br>as legacy bridge| LEGACY
|
||||
|
||||
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
|
||||
linkStyle 0,1,2,3,4 stroke-width:0px
|
||||
|
||||
class APP app
|
||||
class APP_TB,APP_K9 app_module
|
||||
class COMMON common
|
||||
class COMMON_APP common_module
|
||||
class FEATURE feature
|
||||
class FEATURE_ACCOUNT,FEATURE_SETTINGS,FEATURE_ONBOARDING,FEATURE_MAIL feature_module
|
||||
class CORE core
|
||||
class CORE_UI,CORE_COMMON,CORE_ANDROID,CORE_DATABASE,CORE_NETWORK,CORE_TESTING core_module
|
||||
class LIBRARY library
|
||||
class LIB_AUTH,LIB_CRYPTO,LIB_STORAGE library_module
|
||||
class LEGACY legacy
|
||||
class LEGACY_MAIL,LEGACY_BACKEND,LEGACY_K9 legacy_module
|
||||
```
|
||||
|
||||
### Module Types
|
||||
|
||||
#### 📱 App Modules
|
||||
|
||||
The App Modules (`app-thunderbird` and `app-k9mail`) contain the application-specific code, including:
|
||||
- Application entry points and initialization logic
|
||||
- Final dependency injection setup
|
||||
- Navigation configuration
|
||||
- Integration with feature modules solely for that application
|
||||
- App-specific themes and resources (strings, themes, etc.)
|
||||
|
||||
#### 🔄 App Common Module
|
||||
|
||||
The `app-common` module acts as the central hub for shared code between both applications. This module serves as the
|
||||
primary "glue" that binds various `feature` modules together, providing a seamless integration point. It also contains:
|
||||
- Shared application logic
|
||||
- Feature coordination
|
||||
- Common dependency injection setup
|
||||
- Legacy code bridges and adapters
|
||||
|
||||
##### What Should Go in App Common
|
||||
|
||||
The app-common module should contain:
|
||||
|
||||
1. **Shared Application Logic**: Code that's needed by both app modules but isn't specific to any one feature.
|
||||
- Example: `BaseApplication` provides common application initialization, language management, and theme setup.
|
||||
- This avoids duplication between app-thunderbird and app-k9mail.
|
||||
2. **Feature Integration Code**: Code that connects different features together.
|
||||
- Example: Code that coordinates between account and mail features.
|
||||
- This maintains separation between features while allowing them to work together.
|
||||
3. **Common Dependency Injection Setup**: Koin modules that configure dependencies shared by both applications.
|
||||
- Example: `AppCommonModule` includes legacy modules and app-common specific modules.
|
||||
- This ensures consistent dependency configuration across both applications.
|
||||
4. **Legacy Code Bridges/Adapters**: Implementations of interfaces defined in feature modules that delegate to legacy code.
|
||||
- Example: `DefaultAccountProfileLocalDataSource` implements `AccountProfileLocalDataSource` from `feature:account:core` and delegates to legacy account code.
|
||||
- These bridges isolate legacy code and prevent direct dependencies on it from feature modules.
|
||||
|
||||
##### What Should NOT Go in App Common
|
||||
|
||||
The following should NOT be placed in app-common:
|
||||
|
||||
1. **Feature-Specific Business Logic**: Business logic that belongs to a specific feature domain should be in that feature's module.
|
||||
- Example: Mail composition logic should be in `feature:mail`, not in app-common.
|
||||
- This maintains clear separation of concerns and feature independence.
|
||||
2. **UI Components**: UI components should be in core:ui or in feature modules.
|
||||
- Example: A custom button component should be in core:ui, while a mail-specific UI component should be in feature:mail.
|
||||
- This ensures proper layering and reusability.
|
||||
3. **Direct Legacy Code**: Legacy code should remain in legacy modules, with app-common providing bridges.
|
||||
- Example: Don't move legacy mail code into app-common; instead, create a bridge in app-common.
|
||||
- This maintains the separation between legacy and modern code.
|
||||
4. **New Feature Implementations**: New features should be implemented in feature modules, not in app-common.
|
||||
- Example: A new calendar feature should be in `feature:calendar`, not in app-common.
|
||||
- This ensures features can evolve independently.
|
||||
|
||||
##### Decision Criteria for New Contributors
|
||||
|
||||
When deciding whether code belongs in app-common or a feature module, consider:
|
||||
|
||||
1. **Is it shared between both applications?** If yes, it might belong in app-common.
|
||||
2. **Is it specific to a single feature domain?** If yes, it belongs in that feature module.
|
||||
3. **Does it bridge to legacy code?** If yes, it belongs in app-common.
|
||||
4. **Does it coordinate between multiple features?** If yes, it might belong in app-common.
|
||||
5. **Is it a new feature implementation?** If yes, create a new feature module instead.
|
||||
|
||||
Remember that app-common should primarily contain integration code, shared application logic, and bridges to legacy code. Feature-specific logic should be in feature modules, even if used by both applications.
|
||||
|
||||
#### ✨ Feature Modules
|
||||
|
||||
The `feature:*` modules are independent and encapsulate distinct user-facing feature domains. They are designed to be
|
||||
reusable and can be integrated into any application module as needed.
|
||||
|
||||
Feature implementation modules (e.g., `:feature:account:impl`) should ideally not depend directly on other feature
|
||||
implementation modules. Instead, they should depend on the public `:api` module of other features (e.g.,
|
||||
`:feature:someOtherFeature:api`) to access their functionality through defined contracts, see
|
||||
[module structure](module-structure.md#-api-module) for more details.
|
||||
|
||||
When features are complex, they can be split into smaller sub feature modules, addressing specific aspects or
|
||||
functionality within a feature domain:
|
||||
|
||||
- `:feature:account:api`: Public interfaces for account management
|
||||
- `:feature:account:settings:api`: Public interfaces for account settings
|
||||
- `:feature:account:settings:impl`: Concrete implementations of account settings
|
||||
|
||||
#### 🧰 Core Modules
|
||||
|
||||
The `core:*` modules contain foundational functionality used across the application:
|
||||
|
||||
- **core:ui**: UI components, themes, and utilities
|
||||
- **core:common**: Common utilities and extensions
|
||||
- **core:network**: Networking utilities and API client infrastructure
|
||||
- **core:database**: Database infrastructure and utilities
|
||||
- **core:testing**: Testing utilities
|
||||
|
||||
Core modules should only contain generic, reusable components that have no specific business logic.
|
||||
Business objects (e.g., account, mail, etc.) should live in their respective feature modules.
|
||||
|
||||
#### 📚 Library Modules
|
||||
|
||||
The `library:*` modules are for specific implementations that might be used across various features or applications.
|
||||
They could be third-party integrations or complex utilities and eventually shared across multiple projects.
|
||||
|
||||
#### 🔙 Legacy Modules
|
||||
|
||||
The `legacy:*` modules that are still required for the project to function, but don't follow the new project structure.
|
||||
These modules should not be used for new development. The goal is to migrate the functionality of these modules to the
|
||||
new structure over time.
|
||||
|
||||
Similarly the `mail:*` and `backend:*` modules are legacy modules that contain the old mail and backend implementations.
|
||||
These modules are being gradually replaced by the new feature modules.
|
||||
|
||||
The `legacy` modules are integrated into the `app-common` module, allowing them to be used by other parts of the app.
|
||||
The glue code for bridging legacy code to the new modular architecture is also located in the `app-common` module. See
|
||||
[module legacy integration](legacy-module-integration.md) for more details.
|
||||
|
||||
## 🔗 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:
|
||||
|
||||
- **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)
|
||||
|
||||
Rules for module dependencies:
|
||||
- **One-Way Dependencies**: Modules should not depend on each other in a circular manner
|
||||
- **API-Implementation Separation**: Modules should depend on `api` modules, not `implementation` modules, see [module structure](module-structure.md#module-structure)
|
||||
- **Feature Integration**: Features should be integrated through the `app-common` module, which acts as the central integration hub
|
||||
- **Dependency Direction**: Dependencies should flow from app modules to common, then to features, and finally to core and libraries
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph APP[App Modules]
|
||||
direction TB
|
||||
APP_TB["`**:app-thunderbird**<br>Thunderbird for Android`"]
|
||||
APP_K9["`**:app-k9mail**<br>K-9 Mail`"]
|
||||
end
|
||||
|
||||
subgraph COMMON[App Common Module]
|
||||
direction TB
|
||||
COMMON_APP["`**:app-common**<br>Integration Code`"]
|
||||
end
|
||||
|
||||
subgraph FEATURE[Feature Modules]
|
||||
direction TB
|
||||
FEATURE_ACCOUNT_API["`**:feature:account:api**`"]
|
||||
FEATURE_ACCOUNT_IMPL["`**:feature:account:impl**`"]
|
||||
FEATURE_SETTINGS_API["`**:feature:settings:api**`"]
|
||||
FEATURE_K9["`**:feature:k9OnlyFeature:impl**`"]
|
||||
FEATURE_TB["`**:feature:tfaOnlyFeature:impl**`"]
|
||||
end
|
||||
|
||||
subgraph CORE[Core Modules]
|
||||
direction TB
|
||||
CORE_UI_API["`**:core:ui:api**`"]
|
||||
CORE_COMMON_API["`**:core:common:api**`"]
|
||||
end
|
||||
|
||||
subgraph LIBRARY[Library Modules]
|
||||
direction TB
|
||||
LIB_AUTH["`**:library:auth**`"]
|
||||
LIB_STORAGE["`**:library:storage**`"]
|
||||
end
|
||||
|
||||
APP_K9 --> |depends on| COMMON_APP
|
||||
APP_TB --> |depends on| COMMON_APP
|
||||
COMMON_APP --> |uses| FEATURE_ACCOUNT_API
|
||||
COMMON_APP --> |injects/uses impl of| FEATURE_ACCOUNT_IMPL
|
||||
FEATURE_ACCOUNT_IMPL --> FEATURE_ACCOUNT_API
|
||||
COMMON_APP --> |uses| FEATURE_SETTINGS_API
|
||||
APP_K9 --> |injects/uses impl of| FEATURE_K9
|
||||
APP_TB --> |injects/uses impl of| FEATURE_TB
|
||||
FEATURE_ACCOUNT_API --> |uses| CORE_UI_API
|
||||
FEATURE_SETTINGS_API --> |uses| CORE_COMMON_API
|
||||
FEATURE_TB --> |uses| LIB_AUTH
|
||||
FEATURE_K9 --> |uses| LIB_STORAGE
|
||||
CORE_COMMON_API --> |uses| LIB_STORAGE
|
||||
|
||||
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_TB,APP_K9 app_module
|
||||
class COMMON common
|
||||
class COMMON_APP common_module
|
||||
class FEATURE feature
|
||||
class FEATURE_ACCOUNT_API,FEATURE_ACCOUNT_IMPL,FEATURE_SETTINGS_API,FEATURE_MAIL feature_module
|
||||
class CORE core
|
||||
class CORE_UI_API,CORE_COMMON_API core_module
|
||||
class LIBRARY library
|
||||
class LIB_AUTH,LIB_STORAGE library_module
|
||||
|
||||
classDef featureK9 fill:#ffcccc,stroke:#cc0000,color:#000000
|
||||
classDef featureTB fill:#ccccff,stroke:#0000cc,color:#000000
|
||||
class FEATURE_K9 featureK9
|
||||
class FEATURE_TB featureTB
|
||||
```
|
||||
|
||||
423
docs/architecture/module-structure.md
Normal file
423
docs/architecture/module-structure.md
Normal file
|
|
@ -0,0 +1,423 @@
|
|||
# 📦 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:<feature-name>:api` for feature modules
|
||||
- `core:<core-name>: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:<feature-name>:impl` for standard implementations
|
||||
- `feature:<feature-name>:impl-<variant>` for variant-specific implementations
|
||||
- `core:<core-name>: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:<feature-name>:testing` for feature-specific test utilities
|
||||
- `core:<core-name>:testing` for core test utilities
|
||||
- `<module-name>: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:<feature-name>:fake` for feature-specific fake implementations
|
||||
- `core:<core-name>: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:<feature-name>:common` for feature-specific common code
|
||||
- `core:<core-name>: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**<br>Thunderbird for Android`"]
|
||||
APP_K9["`**:app-k9mail**<br>K-9 Mail`"]
|
||||
end
|
||||
|
||||
subgraph COMMON[App Common Module]
|
||||
direction TB
|
||||
COMMON_APP["`**:app-common**<br>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
|
||||
|
||||
688
docs/architecture/theme-system.md
Normal file
688
docs/architecture/theme-system.md
Normal file
|
|
@ -0,0 +1,688 @@
|
|||
# 🎭 Theming
|
||||
|
||||
This document provides a detailed explanation of the theming system used in our applications. It covers the theme
|
||||
architecture, components, customization, and usage.
|
||||
|
||||
- **✨ Material Design 3**: Based on Material Design 3 principles
|
||||
- **🎨 Colors**: Custom color schemes with light and dark modes
|
||||
- **🌓 Dark Mode**: Full support for light and dark themes
|
||||
- **🌈 Dynamic Color**: Support for dynamic color based on system settings
|
||||
- **🪜 Elevations**: Consistent elevation system for shadows
|
||||
- **🖼️ Images**: Images and icons consistent with the theme
|
||||
- **🔶 Shapes**: Customizable shape system for components
|
||||
- **📐 Sizes**: Standardized sizes for components
|
||||
- **📏 Spacings**: Consistent spacing system for layout
|
||||
- **🅰️ Typography**: Consistent typography system
|
||||
|
||||
## 📱 Theme Architecture
|
||||
|
||||
Our theme architecture is designed with several key principles in mind:
|
||||
|
||||
1. **Consistency**: Provide a unified look and feel across all applications while allowing for brand-specific customization
|
||||
2. **Flexibility**: Support different visual identities for different applications (Thunderbird, K-9 Mail) using the same underlying system
|
||||
3. **Extensibility**: Enable easy addition of new theme components or modification of existing ones
|
||||
4. **Maintainability**: Centralize theme definitions to simplify updates and changes
|
||||
5. **Material Design Compatibility**: Build on top of Material Design 3 while extending it with our specific needs
|
||||
|
||||
The theming system follows a hierarchical structure:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph APP_THEMES["App-Specific Themes"]
|
||||
TB_THEME[ThunderbirdTheme2]
|
||||
K9_THEME[K9MailTheme2]
|
||||
end
|
||||
|
||||
subgraph MAIN["Main Theme"]
|
||||
MAIN_THEME[MainTheme]
|
||||
THEME_CONFIG[ThemeConfig]
|
||||
end
|
||||
|
||||
subgraph MATERIAL["Material Design 3"]
|
||||
MAT_THEME[MaterialTheme]
|
||||
end
|
||||
|
||||
TB_THEME --> |uses| MAIN_THEME
|
||||
TB_THEME --> |defines| THEME_CONFIG
|
||||
K9_THEME --> |uses| MAIN_THEME
|
||||
K9_THEME --> |defines| THEME_CONFIG
|
||||
THEME_CONFIG --> |configures| MAIN_THEME
|
||||
MAIN_THEME --> |wraps| MAT_THEME
|
||||
|
||||
classDef app_theme fill:#d9ffd9,stroke:#000000,color:#000000
|
||||
classDef main_theme fill:#d9e9ff,stroke:#000000,color:#000000
|
||||
classDef material fill:#ffe6cc,stroke:#000000,color:#000000
|
||||
|
||||
linkStyle default stroke:#999,stroke-width:2px
|
||||
|
||||
class TB_THEME,K9_THEME app_theme
|
||||
class MAIN_THEME,THEME_CONFIG main_theme
|
||||
class MAT_THEME material
|
||||
```
|
||||
|
||||
### 🏗️ Architecture Layers
|
||||
|
||||
The theme system consists of three main layers:
|
||||
|
||||
1. **App-Specific Themes Layer**: The top layer contains theme implementations for specific applications (ThunderbirdTheme2, K9MailTheme2). Each app theme:
|
||||
- Defines its own brand colors, logos, and other app-specific visual elements
|
||||
- Creates a ThemeConfig with these customizations
|
||||
- Uses the MainTheme as its foundation
|
||||
2. **Main Theme Layer**: The middle layer provides our extended theming system:
|
||||
- MainTheme: A composable function that sets up the theme environment
|
||||
- ThemeConfig: A data class that holds all theme components
|
||||
- This layer extends Material Design with additional components like custom spacings, elevations, and app-specific colors
|
||||
3. **Material Design Layer**: The foundation layer is Material Design 3:
|
||||
- Provides the base theming system (colors, typography, shapes)
|
||||
- Ensures compatibility with standard Material components
|
||||
- Our MainTheme wraps MaterialTheme and converts our theme components to Material 3 format when needed
|
||||
|
||||
### 🔄 Data Flow
|
||||
|
||||
The theme data flows through the system as follows:
|
||||
|
||||
1. App-specific themes (ThunderbirdTheme2, K9MailTheme2) define their visual identity through a ThemeConfig
|
||||
2. ThemeConfig is passed to MainTheme, which:
|
||||
- Selects the appropriate color scheme based on dark/light mode
|
||||
- Configures system bars (status bar, navigation bar)
|
||||
- Provides all theme components through CompositionLocal providers
|
||||
- Converts our theme components to Material 3 format and configures MaterialTheme
|
||||
3. Composables access theme properties through the MainTheme object
|
||||
4. Material components automatically use the Material 3 theme derived from our theme
|
||||
|
||||
### 🌟 Benefits
|
||||
|
||||
This architecture provides several benefits:
|
||||
|
||||
- **Separation of Concerns**: Each layer has a specific responsibility
|
||||
- **Code Reuse**: Common theme logic is shared between applications
|
||||
- **Customization**: Each application can have its own visual identity
|
||||
- **Consistency**: All applications share the same theming structure and components
|
||||
- **Extensibility**: New theme components can be added without changing the overall architecture
|
||||
- **Compatibility**: Works with both our custom components and standard Material components
|
||||
|
||||
## 🧩 Theme Components
|
||||
|
||||
The theming system consists of several components that work together to provide a comprehensive and consistent visual experience across the application. Each component is responsible for a specific aspect of the UI design.
|
||||
|
||||
### 🔧 ThemeConfig
|
||||
|
||||
The `ThemeConfig` is the central configuration class that holds all theme components. It serves as a container for all theme-related settings and is passed to the `MainTheme` composable.
|
||||
|
||||
```kotlin
|
||||
data class ThemeConfig(
|
||||
val colors: ThemeColorSchemeVariants,
|
||||
val elevations: ThemeElevations,
|
||||
val images: ThemeImageVariants,
|
||||
val shapes: ThemeShapes,
|
||||
val sizes: ThemeSizes,
|
||||
val spacings: ThemeSpacings,
|
||||
val typography: ThemeTypography,
|
||||
)
|
||||
```
|
||||
|
||||
The `ThemeConfig` allows for:
|
||||
- Centralized management of all theme components
|
||||
- Easy switching between light and dark themes
|
||||
- Simplified theme customization for different applications
|
||||
- Consistent theme application throughout the app
|
||||
|
||||
### 🎨 ThemeColorScheme
|
||||
|
||||
The `ThemeColorScheme` defines all colors used in the application. It extends Material Design 3's color system with additional colors specific to our applications.
|
||||
|
||||
```kotlin
|
||||
data class ThemeColorScheme(
|
||||
// Material 3 colors
|
||||
val primary: Color,
|
||||
val onPrimary: Color,
|
||||
val primaryContainer: Color,
|
||||
val onPrimaryContainer: Color,
|
||||
val secondary: Color,
|
||||
val onSecondary: Color,
|
||||
val secondaryContainer: Color,
|
||||
val onSecondaryContainer: Color,
|
||||
val tertiary: Color,
|
||||
val onTertiary: Color,
|
||||
val tertiaryContainer: Color,
|
||||
val onTertiaryContainer: Color,
|
||||
val error: Color,
|
||||
val onError: Color,
|
||||
val errorContainer: Color,
|
||||
val onErrorContainer: Color,
|
||||
val surfaceDim: Color,
|
||||
val surface: Color,
|
||||
val surfaceBright: Color,
|
||||
val onSurface: Color,
|
||||
val onSurfaceVariant: Color,
|
||||
val surfaceContainerLowest: Color,
|
||||
val surfaceContainerLow: Color,
|
||||
val surfaceContainer: Color,
|
||||
val surfaceContainerHigh: Color,
|
||||
val surfaceContainerHighest: Color,
|
||||
val inverseSurface: Color,
|
||||
val inverseOnSurface: Color,
|
||||
val inversePrimary: Color,
|
||||
val outline: Color,
|
||||
val outlineVariant: Color,
|
||||
val scrim: Color,
|
||||
|
||||
// Extra colors
|
||||
val info: Color,
|
||||
val onInfo: Color,
|
||||
val infoContainer: Color,
|
||||
val onInfoContainer: Color,
|
||||
val success: Color,
|
||||
val onSuccess: Color,
|
||||
val successContainer: Color,
|
||||
val onSuccessContainer: Color,
|
||||
val warning: Color,
|
||||
val onWarning: Color,
|
||||
val warningContainer: Color,
|
||||
val onWarningContainer: Color,
|
||||
)
|
||||
```
|
||||
|
||||
The color scheme is organized into:
|
||||
- **Base colors**: Primary, secondary, and tertiary colors that define the app's brand identity
|
||||
- **Surface colors**: Colors for backgrounds, cards, and other surfaces
|
||||
- **Content colors**: Colors for text and icons that appear on various backgrounds (prefixed with "on")
|
||||
- **Container colors**: Colors for containers like buttons, chips, and other interactive elements
|
||||
- **Utility colors**: Colors for specific purposes like errors, outlines, and scrims
|
||||
|
||||
Colors are provided in variants for both light and dark themes through the `ThemeColorSchemeVariants` class:
|
||||
|
||||
```kotlin
|
||||
data class ThemeColorSchemeVariants(
|
||||
val light: ThemeColorScheme,
|
||||
val dark: ThemeColorScheme,
|
||||
)
|
||||
```
|
||||
|
||||
### 🪜 ThemeElevations
|
||||
|
||||
The `ThemeElevations` component defines standard elevation values used throughout the application to create a consistent sense of depth and hierarchy.
|
||||
|
||||
```kotlin
|
||||
data class ThemeElevations(
|
||||
val level0: Dp,
|
||||
val level1: Dp,
|
||||
val level2: Dp,
|
||||
val level3: Dp,
|
||||
val level4: Dp,
|
||||
val level5: Dp,
|
||||
)
|
||||
```
|
||||
|
||||
Typical usage includes:
|
||||
- **level0**: For elements that are flush with their background (0dp)
|
||||
- **level1**: For subtle elevation like dividers (1dp)
|
||||
- **level2**: For cards, buttons in their resting state (3dp)
|
||||
- **level3**: For floating action buttons, navigation drawers (6dp)
|
||||
- **level4**: For dialogs, bottom sheets (8dp)
|
||||
- **level5**: For modal surfaces that should appear prominently (12dp)
|
||||
|
||||
### 🖼️ ThemeImages
|
||||
|
||||
The `ThemeImages` component stores references to app-specific images like logos, icons, and illustrations.
|
||||
|
||||
```kotlin
|
||||
data class ThemeImages(
|
||||
val logo: Int, // Resource ID
|
||||
// ... other image resources
|
||||
)
|
||||
```
|
||||
|
||||
These images can have light and dark variants through the `ThemeImageVariants` class:
|
||||
|
||||
```kotlin
|
||||
data class ThemeImageVariants(
|
||||
val light: ThemeImages,
|
||||
val dark: ThemeImages,
|
||||
)
|
||||
```
|
||||
|
||||
### 🔶 ThemeShapes
|
||||
|
||||
The `ThemeShapes` component defines the corner shapes used for UI elements throughout the application.
|
||||
|
||||
```kotlin
|
||||
data class ThemeShapes(
|
||||
val extraSmall: CornerBasedShape,
|
||||
val small: CornerBasedShape,
|
||||
val medium: CornerBasedShape,
|
||||
val large: CornerBasedShape,
|
||||
val extraLarge: CornerBasedShape,
|
||||
)
|
||||
```
|
||||
|
||||
These shapes are used for:
|
||||
- **extraSmall**: Subtle rounding for elements like text fields (4dp)
|
||||
- **small**: Light rounding for cards, buttons (8dp)
|
||||
- **medium**: Moderate rounding for floating elements (12dp)
|
||||
- **large**: Significant rounding for prominent elements (16dp)
|
||||
- **extraLarge**: Very rounded corners for special elements (28dp)
|
||||
|
||||
Note: For no rounding (0% corner radius), use `RectangleShape`. For completely rounded corners (50% corner radius) for circular elements, use `CircleShape`.
|
||||
|
||||
The `ThemeShapes` can be converted to Material 3 shapes using the `toMaterial3Shapes()` method for compatibility with Material components.
|
||||
|
||||
### 📐 ThemeSizes
|
||||
|
||||
The `ThemeSizes` component defines standard size values for UI elements to ensure consistent sizing throughout the application.
|
||||
|
||||
```kotlin
|
||||
data class ThemeSizes(
|
||||
val smaller: Dp,
|
||||
val small: Dp,
|
||||
val medium: Dp,
|
||||
val large: Dp,
|
||||
val larger: Dp,
|
||||
val huge: Dp,
|
||||
val huger: Dp,
|
||||
|
||||
val iconSmall: Dp,
|
||||
val icon: Dp,
|
||||
val iconLarge: Dp,
|
||||
val iconAvatar: Dp,
|
||||
|
||||
val topBarHeight: Dp,
|
||||
val bottomBarHeight: Dp,
|
||||
val bottomBarHeightWithFab: Dp,
|
||||
)
|
||||
```
|
||||
|
||||
These sizes are used for:
|
||||
- **General sizes**: `smaller`, `small`, `medium`, `large`, `larger`, `huge`, `huger` for component dimensions (width, height), button heights, and other UI element dimensions that need standardization
|
||||
- **Icon sizes**: `iconSmall`, `icon`, `iconLarge` for different icon sizes throughout the app
|
||||
- **Avatar size**: `iconAvatar` for user avatars and profile pictures
|
||||
- **Layout sizes**: `topBarHeight`, `bottomBarHeight`, `bottomBarHeightWithFab` for consistent app bar and navigation bar heights
|
||||
|
||||
### 📏 ThemeSpacings
|
||||
|
||||
The `ThemeSpacings` component defines standard spacing values used for margins, padding, and gaps between elements.
|
||||
|
||||
```kotlin
|
||||
data class ThemeSpacings(
|
||||
val zero: Dp,
|
||||
val quarter: Dp,
|
||||
val half: Dp,
|
||||
val default: Dp,
|
||||
val oneHalf: Dp,
|
||||
val double: Dp,
|
||||
val triple: Dp,
|
||||
val quadruple: Dp,
|
||||
)
|
||||
```
|
||||
|
||||
Consistent spacing helps create a rhythmic and harmonious layout:
|
||||
- **zero**: No spacing (0dp)
|
||||
- **quarter**: Quarter of the default spacing, for very tight layouts (4dp)
|
||||
- **half**: Half of the default spacing, for tight layouts (8dp)
|
||||
- **default**: The standard spacing unit for general use (16dp)
|
||||
- **oneHalf**: One and a half times the default spacing (24dp)
|
||||
- **double**: Twice the default spacing, for separating sections (32dp)
|
||||
- **triple**: Three times the default spacing, for major layout divisions (48dp)
|
||||
- **quadruple**: Four times the default spacing, for maximum separation (64dp)
|
||||
|
||||
### 🅰️ ThemeTypography
|
||||
|
||||
The `ThemeTypography` component defines text styles for different types of content throughout the application.
|
||||
|
||||
```kotlin
|
||||
data class ThemeTypography(
|
||||
// Display styles for large headlines
|
||||
val displayLarge: TextStyle,
|
||||
val displayMedium: TextStyle,
|
||||
val displaySmall: TextStyle,
|
||||
|
||||
// Headline styles for section headers
|
||||
val headlineLarge: TextStyle,
|
||||
val headlineMedium: TextStyle,
|
||||
val headlineSmall: TextStyle,
|
||||
|
||||
// Title styles for content titles
|
||||
val titleLarge: TextStyle,
|
||||
val titleMedium: TextStyle,
|
||||
val titleSmall: TextStyle,
|
||||
|
||||
// Body styles for main content
|
||||
val bodyLarge: TextStyle,
|
||||
val bodyMedium: TextStyle,
|
||||
val bodySmall: TextStyle,
|
||||
|
||||
// Label styles for buttons and small text
|
||||
val labelLarge: TextStyle,
|
||||
val labelMedium: TextStyle,
|
||||
val labelSmall: TextStyle,
|
||||
)
|
||||
```
|
||||
|
||||
Each `TextStyle` includes:
|
||||
- Font family
|
||||
- Font weight
|
||||
- Font size
|
||||
- Line height
|
||||
- Letter spacing
|
||||
- Other typographic attributes
|
||||
|
||||
The `ThemeTypography` can be converted to Material 3 typography using the `toMaterial3Typography()` method for compatibility with Material components.
|
||||
|
||||
### ↔️ Component Interaction
|
||||
|
||||
These theme components work together to create a cohesive design system:
|
||||
|
||||
1. **ThemeConfig** aggregates all components and provides them to the `MainTheme`
|
||||
2. **MainTheme** makes components available through `CompositionLocal` providers
|
||||
3. Composables access theme components through the `MainTheme` object
|
||||
4. Components like `ThemeColorScheme` and `ThemeShapes` are converted to Material 3 equivalents for use with Material components
|
||||
|
||||
This structured approach ensures consistent design application throughout the app while providing flexibility for customization.
|
||||
|
||||
## 🌟 MainTheme
|
||||
|
||||
The `MainTheme` is the foundation of our theming system:
|
||||
|
||||
- Acts as a wrapper around Material Design 3's `MaterialTheme`
|
||||
- Provides additional theme components beyond what Material Design offers
|
||||
- Configurable through a `ThemeConfig` parameter
|
||||
- Supports dark mode and dynamic color
|
||||
- Exposes theme components through the `MainTheme` object
|
||||
|
||||
### 🔌 Theme Provider Implementation and Usage
|
||||
|
||||
#### 🛠️ How the Theme Provider Works
|
||||
|
||||
The `MainTheme` function uses Jetpack Compose's `CompositionLocalProvider` to make theme components available throughout the composition tree:
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
fun MainTheme(
|
||||
themeConfig: ThemeConfig,
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val themeColorScheme = selectThemeColorScheme(
|
||||
themeConfig = themeConfig,
|
||||
darkTheme = darkTheme,
|
||||
dynamicColor = dynamicColor,
|
||||
)
|
||||
val themeImages = selectThemeImages(
|
||||
themeConfig = themeConfig,
|
||||
darkTheme = darkTheme,
|
||||
)
|
||||
|
||||
SystemBar(
|
||||
darkTheme = darkTheme,
|
||||
colorScheme = themeColorScheme,
|
||||
)
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalThemeColorScheme provides themeColorScheme,
|
||||
LocalThemeElevations provides themeConfig.elevations,
|
||||
LocalThemeImages provides themeImages,
|
||||
LocalThemeShapes provides themeConfig.shapes,
|
||||
LocalThemeSizes provides themeConfig.sizes,
|
||||
LocalThemeSpacings provides themeConfig.spacings,
|
||||
LocalThemeTypography provides themeConfig.typography,
|
||||
) {
|
||||
MaterialTheme(
|
||||
colorScheme = themeColorScheme.toMaterial3ColorScheme(),
|
||||
shapes = themeConfig.shapes.toMaterial3Shapes(),
|
||||
typography = themeConfig.typography.toMaterial3Typography(),
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Each theme component is provided through a `CompositionLocal` that makes it available to all composables in the composition tree. These `CompositionLocal` values are defined using `staticCompositionLocalOf` in their respective files:
|
||||
|
||||
```kotlin
|
||||
internal val LocalThemeColorScheme = staticCompositionLocalOf<ThemeColorScheme> {
|
||||
error("No ThemeColorScheme provided")
|
||||
}
|
||||
|
||||
internal val LocalThemeElevations = staticCompositionLocalOf<ThemeElevations> {
|
||||
error("No ThemeElevations provided")
|
||||
}
|
||||
|
||||
// ... other LocalTheme* definitions
|
||||
```
|
||||
|
||||
The `MainTheme` object provides properties to access these values from anywhere in the composition tree:
|
||||
|
||||
```kotlin
|
||||
object MainTheme {
|
||||
val colors: ThemeColorScheme
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
get() = LocalThemeColorScheme.current
|
||||
|
||||
val elevations: ThemeElevations
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
get() = LocalThemeElevations.current
|
||||
|
||||
// ... other properties
|
||||
}
|
||||
```
|
||||
|
||||
This theme provider mechanism ensures that theme components are available throughout the app without having to pass them as parameters to every composable.
|
||||
|
||||
## 🎭 App-Specific Themes
|
||||
|
||||
The app-specific themes (`ThunderbirdTheme2` and `K9MailTheme2`) customize the `MainTheme` for each application:
|
||||
|
||||
- Provide app-specific color schemes
|
||||
- Include app-specific assets (like logos)
|
||||
- Configure theme components through `ThemeConfig`
|
||||
- Use default values for common components (elevations, sizes, spacings, shapes, typography)
|
||||
|
||||
### ThunderbirdTheme2
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
fun ThunderbirdTheme2(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
dynamicColor: Boolean = false,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val images = ThemeImages(
|
||||
logo = R.drawable.core_ui_theme2_thunderbird_logo,
|
||||
)
|
||||
|
||||
val themeConfig = ThemeConfig(
|
||||
colors = ThemeColorSchemeVariants(
|
||||
dark = darkThemeColorScheme,
|
||||
light = lightThemeColorScheme,
|
||||
),
|
||||
elevations = defaultThemeElevations,
|
||||
images = ThemeImageVariants(
|
||||
light = images,
|
||||
dark = images,
|
||||
),
|
||||
sizes = defaultThemeSizes,
|
||||
spacings = defaultThemeSpacings,
|
||||
shapes = defaultThemeShapes,
|
||||
typography = defaultTypography,
|
||||
)
|
||||
|
||||
MainTheme(
|
||||
themeConfig = themeConfig,
|
||||
darkTheme = darkTheme,
|
||||
dynamicColor = dynamicColor,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### K9MailTheme2
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
fun K9MailTheme2(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
dynamicColor: Boolean = false,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val images = ThemeImages(
|
||||
logo = R.drawable.core_ui_theme2_k9mail_logo,
|
||||
)
|
||||
|
||||
val themeConfig = ThemeConfig(
|
||||
colors = ThemeColorSchemeVariants(
|
||||
dark = darkThemeColorScheme,
|
||||
light = lightThemeColorScheme,
|
||||
),
|
||||
elevations = defaultThemeElevations,
|
||||
images = ThemeImageVariants(
|
||||
light = images,
|
||||
dark = images,
|
||||
),
|
||||
sizes = defaultThemeSizes,
|
||||
spacings = defaultThemeSpacings,
|
||||
shapes = defaultThemeShapes,
|
||||
typography = defaultTypography,
|
||||
)
|
||||
|
||||
MainTheme(
|
||||
themeConfig = themeConfig,
|
||||
darkTheme = darkTheme,
|
||||
dynamicColor = dynamicColor,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 🎨 Using Themes in the App
|
||||
|
||||
### 🧩 Applying a Theme
|
||||
|
||||
To apply a theme to your UI, wrap your composables with the appropriate theme composable:
|
||||
|
||||
```kotlin
|
||||
// For Thunderbird app
|
||||
@Composable
|
||||
fun ThunderbirdApp() {
|
||||
ThunderbirdTheme2 {
|
||||
// App content
|
||||
}
|
||||
}
|
||||
|
||||
// For K9Mail app
|
||||
@Composable
|
||||
fun K9MailApp() {
|
||||
K9MailTheme2 {
|
||||
// App content
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 🔑 Accessing Theme Components
|
||||
|
||||
Inside themed content, you can access theme properties through the `MainTheme` object:
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
fun ThemedButton(
|
||||
text: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Button(
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MainTheme.colors.primary,
|
||||
contentColor = MainTheme.colors.onPrimary,
|
||||
),
|
||||
shape = MainTheme.shapes.medium,
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MainTheme.typography.labelLarge,
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🌓 Dark Mode and Dynamic Color
|
||||
|
||||
The theming system supports both dark mode and dynamic color:
|
||||
|
||||
- **Dark Mode**: Automatically applies the appropriate color scheme based on the system's dark mode setting
|
||||
- **Dynamic Color**: Optionally uses the device's wallpaper colors for the theme (Android 12+)
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
fun ThunderbirdTheme2(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(), // Default to system setting
|
||||
dynamicColor: Boolean = false, // Disabled by default
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 Customizing Themes
|
||||
|
||||
To customize a theme, you can create a new theme composable that wraps `MainTheme` with your custom `ThemeConfig`:
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
fun CustomTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
dynamicColor: Boolean = false,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val images = ThemeImages(
|
||||
logo = R.drawable.custom_logo,
|
||||
)
|
||||
|
||||
val themeConfig = ThemeConfig(
|
||||
colors = ThemeColorSchemeVariants(
|
||||
dark = customDarkThemeColorScheme,
|
||||
light = customLightThemeColorScheme,
|
||||
),
|
||||
elevations = customThemeElevations,
|
||||
images = ThemeImageVariants(
|
||||
light = images,
|
||||
dark = images,
|
||||
),
|
||||
sizes = customThemeSizes,
|
||||
spacings = customThemeSpacings,
|
||||
shapes = customThemeShapes,
|
||||
typography = customTypography,
|
||||
)
|
||||
|
||||
MainTheme(
|
||||
themeConfig = themeConfig,
|
||||
darkTheme = darkTheme,
|
||||
dynamicColor = dynamicColor,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 🧪 Testing with Themes
|
||||
|
||||
When writing tests for composables that use theme components, you need to wrap them in a theme:
|
||||
|
||||
```kotlin
|
||||
@Test
|
||||
fun testThemedButton() {
|
||||
composeTestRule.setContent {
|
||||
ThunderbirdTheme2 {
|
||||
ThemedButton(
|
||||
text = "Click Me",
|
||||
onClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText("Click Me").assertExists()
|
||||
}
|
||||
```
|
||||
|
||||
1302
docs/architecture/ui-architecture.md
Normal file
1302
docs/architecture/ui-architecture.md
Normal file
File diff suppressed because it is too large
Load diff
24
docs/architecture/user-flows.md
Normal file
24
docs/architecture/user-flows.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# User Flows
|
||||
|
||||
The user flows diagrams below illustrate typical paths users take through the application, helping developers understand how different components interact from a user perspective.
|
||||
|
||||
For information about the repository structure and module organization, see the [Project Structure document](project-structure.md).
|
||||
|
||||
## Mail
|
||||
|
||||
### Reading email
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### Sending email
|
||||
|
||||

|
||||
|
||||
## Verifying Flows
|
||||
|
||||
We plan to test these user flows using [maestro](https://maestro.dev/), a tool for automating UI tests. Maestro allows us to write tests in a
|
||||
simple YAML format, making it easy to define user interactions and verify application behavior.
|
||||
|
||||
The current flows could be found in the *`ui-flows` directory in the repository.
|
||||
Loading…
Add table
Add a link
Reference in a new issue