Architecture Overview
This project follows the official Android Architecture Guidelines with some pragmatic adaptations to keep the codebase simple and maintainable.
Architectural Principles
The architecture is built on several key principles:
- Separation of Concerns: Each component has its own responsibility
- Single Source of Truth: Data is managed in a single place
- Unidirectional Data Flow: Data flows in one direction, events flow in the opposite
- State-Based UI: UI is a reflection of the state
- Pragmatic Simplicity: Complex patterns are only added when necessary
Core Layers
The app uses a two-layer architecture:
graph TD
A[UI Layer] --> B[Data Layer]
style A fill: #4CAF50, stroke: #333, stroke-width: 2px
style B fill: #1976D2, stroke: #333, stroke-width: 2px
UI Layer
The UI layer follows MVVM pattern and consists of:
- Composable UI: Pure UI components built with Jetpack Compose
- ViewModel: Manages UI state and business logic
- UI State: Immutable data classes representing screen state
Example UI Layer structure:
data class HomeScreenData(
val items: List<Item> = emptyList(),
// other UI state properties
)
@HiltViewModel
class HomeViewModel @Inject constructor(
private val repository: HomeRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(UiState(HomeScreenData()))
val uiState = _uiState.asStateFlow()
}
@Composable
fun HomeScreen(
uiState: HomeScreenData,
onAction: (HomeAction) -> Unit
) {
}
Data Layer
The data layer handles data operations and consists of:
- Repositories: Single source of truth for data
- Data Sources: Interface with external systems (API, database, etc.)
- Models: Data representation classes
Example Data Layer structure:
class HomeRepositoryImpl @Inject constructor(
private val localDataSource: LocalDataSource,
private val networkDataSource: NetworkDataSource
) : HomeRepository {
override fun getData(): Flow<List<Data>> =
networkBoundResource(
query = { localDataSource.getData() },
fetch = { networkDataSource.getData() },
saveFetchResult = { localDataSource.saveData(it) }
)
}
Note
Unlike the official guidelines, this project intentionally omits the domain layer to reduce complexity. You can add a domain layer if your app requires complex business logic or needs to share logic between multiple ViewModels.
State Management
The project uses a consistent state management pattern:
-
UiState Wrapper:
-
State Updates:
-
State Display:
Dependency Injection
The project uses Hilt for dependency injection:
- Modules: Organized by feature and core functionality
- Scoping: Primarily uses singleton scope for repositories and data sources
- Testing: Enables easy dependency replacement for testing
Data Flow
- User Interaction → UI Events
- ViewModel → Business Logic
- Repository → Data Operations
- DataSource → External Systems
- Back to UI through StateFlow
graph LR
A[User Action] --> B[ViewModel]
B --> C[Repository]
C --> D[Data Sources]
D --> C
C --> B
B --> E[UI State]
E --> F[UI]
Adding a Domain Layer
If your app grows in complexity, you can add a domain layer:
class GetDataUseCase @Inject constructor(
private val repository: Repository
) {
suspend operator fun invoke(params: Params): Result<Data> =
repository.getData(params)
}
Tip
Consider adding a domain layer when: - Multiple ViewModels share business logic - Business rules become complex - You need to transform data between layers
Testing Strategy (Upcoming 🚧)
The architecture enables different types of tests:
- UI Tests: Test Composables in isolation
- ViewModel Tests: Test state management and business logic
- Repository Tests: Test data operations
- Integration Tests: Test multiple layers together
Best Practices
- Keep UI State Simple: Only include what's needed for the UI
- Single Responsibility: Each class should have one clear purpose
- Error Handling: Use Result type for operations that can fail
- Coroutines: Use structured concurrency with proper scoping
- Immutable Data: Use data classes for state and models
Further Reading
- Design Philosophy: Understand the design principles behind the architecture
- Adding New Features: Learn how to add new features to the project
- CI/CD Setup: Set up continuous integration and deployment for the project
- Performance Optimization: Optimize the app for speed and efficiency
- Useful Tips & Tricks: Get useful tips for development and debugging
- Convention Plugins: Learn about custom Gradle plugins used in the project
- Code Style with Spotless: Follow code formatting guidelines for the project