This project prioritizes pragmatic simplicity over theoretical purity, making conscious trade-offs that favor maintainability and readability over absolute correctness or flexibility.
We favor straightforward, understandable solutions over complex but theoretically "pure" ones. This means:
- Using direct state management approaches
- Minimizing layers when possible
- Keeping code readable and debuggable
All UI state is wrapped in a consistent structure:
data class UiState<T : Any>(
val data: T,
val loading: Boolean = false,
val error: OneTimeEvent<Throwable?> = OneTimeEvent(null)
)
Instead of implementing error and loading states individually for each screen, we handle these centrally:
@Composable
fun <T : Any> StatefulComposable(
state: UiState<T>,
onShowSnackbar: suspend (String, SnackbarAction, Throwable?) -> Boolean,
content: @Composable (T) -> Unit
) {
// Centralized loading and error handling
content(state.data)
}
Note
This approach trades some flexibility for consistency and reduced boilerplate.
We opt for a direct approach to state management in ViewModels:
class AuthViewModel @Inject constructor(
private val authRepository: AuthRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(UiState(AuthScreenData()))
val uiState = _uiState.asStateFlow()
fun updateEmail(email: String) {
_uiState.updateState {
copy(
email = TextFiledData(
value = email,
errorMessage = email.validateEmail()
)
)
}
}
}
Trade-offs Made:
- ✅ Readability: State changes are explicit and easy to trace
- ✅ Debuggability: Direct state mutations are easier to debug
- ✅ Simplicity: Easier to manage multiple UI events
- ❌ Purity: Less adherence to functional programming principles
We use a consistent error handling approach throughout the app:
-
Repository Layer: Uses
Result
typesuspend fun getData(): Result<Data> = suspendRunCatching { dataSource.getData() }
-
ViewModel Layer: Converts to
OneTimeEvent
viewModelScope.launch { repository.getData() .onSuccess { data -> _uiState.updateState { copy(data = data) } } .onFailure { error -> _uiState.updateState { copy(error = OneTimeEvent(error)) } } }
-
UI Layer: Handled by
StatefulComposable
Tip
This standardized approach makes error handling predictable across the entire application.
We provide extension functions for common state update patterns:
// Regular state updates
_uiState.updateState {
copy(value = newValue)
}
// Async operations with loading state
_uiState.updateStateWith(viewModelScope) {
repository.someAsyncOperation()
}
Each feature is self-contained and follows a consistent structure:
feature/
└── auth/
├── navigation/ # Navigation-related code
├── ui/ # UI components and ViewModels
└── model/ # Feature-specific models
Our design philosophy makes several conscious trade-offs:
-
Simplicity vs. Flexibility
- We choose simpler solutions even if they're less flexible
- Custom solutions are added only when really needed
-
Convention vs. Configuration
- We favor strong conventions over configuration options
- This reduces decision fatigue but may limit customization
-
Pragmatism vs. Purity
- We prioritize practical solutions over theoretical purity
- This may mean occasionally breaking "clean" architecture rules
-
Consistency vs. Optimization
- We prefer consistent patterns across the codebase
- This might mean using the same solution even when a specialized one might be marginally better
Important
These patterns are guidelines, not rules. The goal is to make the codebase more maintainable and easier to understand, not to restrict flexibility where it's truly needed.
-
Reduced Cognitive Load
- Developers can predict where to find things
- Common patterns reduce decision fatigue
-
Easier Onboarding
- New team members can quickly understand the codebase
- Consistent patterns make learning curve smoother
-
Better Maintainability
- Common patterns make code more predictable
- Centralized handling reduces bugs
-
Improved Debugging
- State changes are explicit and traceable
- Error handling is consistent and predictable
While these patterns serve well for most cases, consider alternatives when:
- A feature has unique error/loading UI requirements
- Performance optimizations are crucial
- Complex business logic demands more separation
- Third-party integration requires different patterns
Tip
When deviating from these patterns, document your reasoning to help other developers understand the context of your decisions.