This document provides best practices and recommendations for Android architecture to improve app quality, robustness, scalability, maintainability, and testability. These are recommendations, not strict requirements, and should be adapted as needed. Recommendations are categorized by priority:
- Strongly recommended: Implement unless there's a fundamental clash with your approach.
- Recommended: Likely to improve your app.
- Optional: Can improve your app in certain situations.
Familiarity with general Android architecture guidance is assumed.
The recommended layered architecture promotes separation of concerns, driving UI from data models, adhering to the single source of truth principle, and following unidirectional data flow.
- Strongly recommended: Use a clearly defined data layer. This layer exposes application data and contains most business logic. Create repositories even with a single data source. In small apps, place data layer types in a
datapackage or module. - Strongly recommended: Use a clearly defined UI layer. This layer displays data and handles user interaction. In small apps, place UI layer types in a
uipackage or module. More UI layer best practices are detailed later. - Strongly recommended: The data layer should expose data via a repository. UI components (composables, activities, ViewModels) should not interact directly with data sources (databases, DataStore, network, etc.).
- Strongly recommended: Use coroutines and flows for inter-layer communication. More coroutines best practices are available elsewhere.
- Recommended (in big apps): Use a domain layer and use cases for reusable business logic across multiple ViewModels or to simplify ViewModel logic.
The UI layer displays data and handles user interaction.
- Strongly recommended: Follow Unidirectional Data Flow (UDF). ViewModels expose UI state using the observer pattern and receive actions via method calls.
- Strongly recommended: Use AAC ViewModels for business logic and fetching data for UI state. More ViewModel best practices are detailed later. Benefits of ViewModels are discussed elsewhere.
- Strongly recommended: Use lifecycle-aware UI state collection. Use
repeatOnLifecyclein the View system andcollectAsStateWithLifecyclein Jetpack Compose. In your composable, you can use a when expression to handle the state as follows:
val uiState = viewModel.uiState.collectAsStateWithLifecycle()
when (uiState) {
ScreenState.Initial -> {
// Show initial state
}
ScreenState.Generating -> {
// Show generating state
}
is ScreenState.Success -> {
// Show success state
}
is ScreenState.Error -> {
// Show error state
}
}- Strongly recommended: Do not send events from the ViewModel to the UI. Process events in the ViewModel and update the state accordingly. More on UI events is available elsewhere.
- Strongly recommended: Every composable function (except top level screen composable) should take a
Modifieras a parameter with a default value. It should be positionned as the first optional parameter. - Recommended: Use a single-activity application. Use Navigation Fragments or Navigation Compose for navigation and deep linking.
- Recommended: Use Jetpack Compose for new apps on phones, tablets, foldables, and Wear OS.
ViewModels provide UI state and data layer access.
- Strongly recommended: ViewModels should be agnostic of the Android lifecycle. Avoid references to lifecycle-related types, Activities, Fragments, Context, or Resources. Evaluate if dependencies on Context belong in the ViewModel.
- Strongly recommended: ViewModel functions should never take an activity as a parameter to avoid memory leaks.
- Strongly recommended: Use coroutines and flows. ViewModels interact with data/domain layers using Kotlin flows for receiving data and
suspendfunctions withviewModelScopefor actions. - Strongly recommended: Use ViewModels at the screen level. Avoid using them in reusable UI components. Use them in screen-level composables, Activities/Fragments, and Navigation destinations/graphs.
- Strongly recommended: Use plain state holder classes in reusable UI components. This allows external control over the state.
- Strongly recommended: ViewModel should only expose one UI state defined as a sealed class. e.g:
sealed class ScreenState {
data object Initial : ScreenState()
data object Generating : ScreenState() // Use for generating content
data class Success(val data: String) : ScreenState() // Use to display data
data class Error(val message: String) : ScreenState() // Use for error state
}
- Recommended: Do not use
AndroidViewModel. Use theViewModelclass. Avoid using theApplicationclass in ViewModels; move the dependency to the UI or data layer. - Recommended: Don't use
LiveData, use state flow instead. - Recommended: Expose a UI state. Use a single
uiStateproperty (aStateFlow) for data exposure. Multiple properties can be used for unrelated data. UsestateInwithWhileSubscribed(5000)for data streams. For simpler cases, use aMutableStateFlowexposed as an immutableStateFlow. Consider using a data class or sealed class for theUiState. - Recommeded: Don’t pass
Contextto yourViewModel. To avoid memory leaks only UI (Composables) should have a reference tocontext. - Recommeded: Don’t use
fetchData()in a ViewModelinit {}block. If you do end up in a case where you need to do something on initial load, you usestateIn()instead.
Best practices for working with the Android lifecycle:
- Strongly recommended: Do not override lifecycle methods (e.g.,
onResume) in Activities or Fragments. UseLifecycleObserverinstead. For lifecycle-dependent work, userepeatOnLifecycle.
Best practices for dependency management:
- Strongly recommended: Use dependency injection, primarily constructor injection.
- Strongly recommended: Scope to a dependency container when sharing mutable data or for expensive, widely used types.
- Recommended: Use Hilt for complex projects or manual dependency injection for simpler apps. Hilt is beneficial for multiple ViewModels, WorkManager usage, and advanced Navigation.
Best practices for testing:
- Strongly recommended: Know what to test. Test ViewModels (including Flows), data layer entities (repositories and data sources), and UI navigation for regression testing.
- Strongly recommended: Prefer fakes over mocks. See the Android documentation on test doubles.
- Strongly recommended: Test StateFlows. Assert on the
valueproperty when possible. Create acollectJobwhen usingWhileSubscribed.
Best practices for models:
- Recommended: Create a model per layer in complex apps. Consider creating separate models for different layers (e.g., remote data source, repositories, ViewModels) to simplify data structures and reduce dependencies.
Optional naming conventions:
- Methods: Verb phrases (e.g.,
makePayment()). - Properties: Noun phrases (e.g.,
inProgressTopicSelection). - Streams:
get{Model}Stream()(e.g.,getAuthorStream(): Flow<Author>,getAuthorsStream(): Flow<List<Author>>). - Interface Implementations: Meaningful names. Prefix with
Defaultif no better name is found (e.g.,OfflineFirstNewsRepository,InMemoryNewsRepository,DefaultNewsRepository). Prefix fake implementations withFake(e.g.,FakeAuthorsRepository).