Recommendations for Android architecture

This page presents several Architecture best practices and recommendations. Adopt them to improve your app's quality, robustness, and scalability. They also make it easier to maintain and test your app.

The best practices below are grouped by topic. Each has a priority that reflects how strong the recommendation is. The list of priorities is as follows:

  • Strongly recommended: Implement this practice unless it clashes fundamentally with your approach.
  • Recommended: This practice is likely to improve your app.
  • Optional: This practice can improve your app in certain circumstances.

Layered architecture

Our recommended layered architecture favors separation of concerns. It drives UI from data models, complies with the single source of truth principle, and follows unidirectional data flow principles. Here are some best practices for layered architecture:

Recommendation Description
Use a clearly defined data layer. The data layer exposes application data to the rest of the app and contains the vast majority of your app's business logic.
  • Create repositories even if they contain only a single data source.
  • In small apps, you can choose to place data layer types in a data package or module.
Use a clearly defined UI layer. The UI layer displays the application data on the screen and serves as the primary point of user interaction. Jetpack Compose is the recommended modern toolkit for building your app's UI.
  • In small apps, you can choose to place data layer types in a ui package or module.
For more information on UI layer best practices, see UI layer.
Expose application data from the data layer using a repository.

Make sure components in the UI layer such as composables or ViewModels don't interact directly with a data source. Examples of data sources include:

  • Databases, DataStore, SharedPreferences, Firebase APIs.
  • GPS location providers.
  • Bluetooth data providers.
  • Network connectivity status providers.
Use coroutines and flows. Use coroutines and flows to communicate between layers.

For more information on coroutines best practices, see Best practices for coroutines in Android.

Use a domain layer. Use a domain layer with use cases if you need to reuse business logic that interacts with the data layer across multiple ViewModels, or you want to simplify the business logic complexity of a particular ViewModel

UI layer

The role of the UI layer is to display the application data on the screen and serve as the primary point of user interaction. Here are some best practices for the UI layer:

Recommendation Description
Follow Unidirectional Data Flow (UDF). Follow Unidirectional Data Flow (UDF) principles, where ViewModels expose UI state using the observer pattern and receive actions from the UI through method calls.
Use AAC ViewModels if their benefits apply to your app. Use AAC ViewModels to handle business logic, and fetch application data to expose UI state to the UI.

For more information on ViewModel best practices, see Architecture recommendations.

For more information on the benefits of ViewModels, see The ViewModel as a business logic state holder.

Use lifecycle-aware UI state collection. Collect UI state from the UI using the appropriate lifecycle-aware coroutine builder, collectAsStateWithLifecycle.

Read more about collectAsStateWithLifecycle.

Do not send events from the ViewModel to the UI. Process the event immediately in the ViewModel and cause a state update with the result of handling the event. For more information about UI events, see Handle ViewModel events.
Use a single-activity application. Use Navigation 3 to navigate between screens and deep link to your app if your app has more than one screen.
Use Jetpack Compose. Use Jetpack Compose to build new apps for phones, tablets, foldables, and Wear OS.

The following snippet outlines how to collect the UI state in a lifecycle-aware manner:

  @Composable
  fun MyScreen(
      viewModel: MyViewModel = viewModel()
  ) {
      val uiState by viewModel.uiState.collectAsStateWithLifecycle()
  }

ViewModel

ViewModels are responsible for providing the UI state and accessing the data layer. Here are some best practices for ViewModels:

Recommendation Description
Keep ViewModels independent of the Android lifecycle. In ViewModels, don't hold a reference to any lifecycle-related type. Don't pass Activity, Context or Resources as a dependency. If something needs a Context in the ViewModel, carefully evaluate if that is in the right layer.
Use coroutines and flows.

The ViewModel interacts with the data or domain layers using the following:

  • Kotlin flows for receiving application data
  • suspend functions for performing actions using viewModelScope
Use ViewModels at screen level.

Do not use ViewModels in reusable pieces of UI. You should use ViewModels in:

  • Screen-level composables,
  • Activities/Fragments in Views,
  • Destinations or graphs when using Jetpack Navigation.
Use plain state holder classes in reusable UI components. Use plain state holder classes for handling complexity in reusable UI components. When you do this, the state can be hoisted and controlled externally.
Do not use AndroidViewModel. Use the ViewModel class, not AndroidViewModel. Don't use the Application class in the ViewModel. Instead, move the dependency to the UI or the data layer.
Expose a UI state. Make your ViewModels expose data to the UI through a single property called uiState. If the UI shows multiple, unrelated pieces of data, the VM can expose multiple UI state properties.
  • Make uiState a StateFlow.
  • Create the uiState using the stateIn operator with the WhileSubscribed(5000) policy if the data comes as a stream of data from other layers of the hierarchy. (See this code example.)
  • For simpler cases with no streams of data coming from the data layer, it's acceptable to use a MutableStateFlow exposed as an immutable StateFlow.
  • You can choose to have the ${Screen}UiState as a data class that can contain data, errors, and loading signals. This class can also be a sealed class if the different states are exclusive.

The following snippet outlines how to expose UI state from a ViewModel:

@HiltViewModel
class BookmarksViewModel @Inject constructor(
    newsRepository: NewsRepository
) : ViewModel() {

    val feedState: StateFlow<NewsFeedUiState> =
        newsRepository
            .getNewsResourcesStream()
            .mapToFeedState(savedNewsResourcesState)
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5_000),
                initialValue = NewsFeedUiState.Loading
            )

    // ...
}

Lifecycle

Follow best practices for working with the Activity lifecycle:

Recommendation Description
Use lifecycle-aware effects in composables instead of overriding Activity lifecycle callbacks.

Don't override Activity lifecycle methods, such as onResume, to run UI-related tasks. Instead, use Compose's LifecycleEffects or lifecycle-aware coroutine scopes:

The following snippet outlines how to perform operations given a certain Lifecycle state:

  @Composable
  fun LocationChangedEffect(
    locationManager: LocationManager,
    onLocationChanged: (Location) -> Unit
  ) {
    val currentOnLocationChanged by rememberUpdatedState(onLocationChanged)

    LifecycleStartEffect(locationManager) {
        val listener = LocationListener { newLocation ->
            currentOnLocationChanged(newLocation)
        }

        try {
            locationManager.requestLocationUpdates(
                LocationManager.GPS_PROVIDER,
                1000L,
                1f,
                listener,
            )
        } catch (e: SecurityException) {
            // TODO: Handle missing permissions
        }

        onStopOrDispose {
            locationManager.removeUpdates(listener)
        }
    }
  }

Handle dependencies

Follow best practices when managing dependencies between components:

Recommendation Description
Use dependency injection. Use dependency injection best practices, mainly constructor injection when possible.
Scope to a component when necessary. Scope to a dependency container when the type contains mutable data that needs to be shared or the type is expensive to initialize and is widely used in the app.
Use Hilt. Use Hilt or manual dependency injection in simple apps. Use Hilt if your project is complex enough-for example, if it includes any of the following:
  • Multiple screens with ViewModels
  • Uses WorkManager
  • Has ViewModels scoped to the navigation back stack

Testing

The following are some best practices for testing:

Recommendation Description
Know what to test.

Unless the project is as simple as a "hello world" app, test it. At minimum, include the following:

  • Unit tests for ViewModels, including Flows
  • Unit tests for data layer entities-that is, repositories and data sources
  • UI navigation tests that are useful as regression tests in CI
Prefer fakes to mocks. For more information on using fakes, see Use test doubles in Android.
Test StateFlows. When testing StateFlow, do the following:

For more information, see What to test in Android and Test your Compose layout.

Models

Observe these best practices when developing models in your apps:

Recommendation Description
Create a model per layer in complex apps.

In complex apps, create new models in different layers or components when it makes sense. Consider the following examples:

  • A remote data source can map the model that it receives through the network to a simpler class with only the data the app needs.
  • Repositories can map DAO models to simpler data classes with just the information the UI layer needs.
  • ViewModel can include data layer models in UiState classes.

Naming conventions

When naming your codebase, you should be aware of the following best practices:

Recommendation Description
Naming methods.
Optional
Use verb phrases to name methods-for example, makePayment().
Naming properties.
Optional
Use noun phrases to name properties-for example, inProgressTopicSelection.
Naming streams of data.
Optional
When a class exposes a Flow stream or any other stream, the naming convention is get{model}Stream. For example, getAuthorStream(): Flow<Author>. If the function returns a list of models, use the plural model name: getAuthorsStream(): Flow<List<Author>>.
Naming interfaces implementations.
Optional
Use meaningful names for the implementations of interfaces. Use Default as the prefix if a better name cannot be found. For example, for a NewsRepository interface, you might have an OfflineFirstNewsRepository, or InMemoryNewsRepository. If you cannot find a good name, use DefaultNewsRepository. Prefix fake implementations with Fake, as in FakeAuthorsRepository.

Additional resources

For more information about Android architecture, see the following additional resources:

Documentation

Views content