[ Web Proxy ]
URL:
Viewing: http://developer.android.com/topic/modularization/patterns [Back]  [Original]

Common modularization patterns  |  App architecture  |  Android Developers Skip to main content
Essentials Design & Plan Develop Google Play Blog
Search:
Android Studio
Android Developers [Android Developers]

Common modularization patterns Stay organized with collections Save and categorize content based on your preferences.

There is no single modularization strategy that fits all projects. Due to the flexible nature of Gradle there are few constraints as to how you can organize a project. This page gives an overview of some general rules and common patterns that you can employ when developing multi module Android apps.

Note: The recommendations and best practices present in this page can be applied to a broad spectrum of apps to allow them to scale, improve quality and robustness, and make them easier to test. However, you should treat them as guidelines and adapt them to your requirements as needed.

High cohesion and low coupling principle

One way of characterizing a modular codebase would be to use the coupling and cohesion properties. Coupling measures the degree to which modules depend on each other. Cohesion, in this context, measures how the elements of a single module are functionally related. As a general rule, you should strive for low coupling and high cohesion:

Tip: If two modules heavily rely on knowledge of each other, it may be a good sign that they should actually act as one system. Conversely, if two parts of a module dont interact with each other often, they should probably be separate modules.

Types of modules

The way you organize your modules mainly depends on your app architecture. Below are some common types of modules you could introduce in your app while following our recommended app architecture.

Note: This section assumes that you are familiar with the concepts outlined in our guide to app architecture.

Data modules

A data module usually contains a repository, data sources and model classes. The three primary responsibilities of a data module are:

  1. Encapsulate all data and business logic of a certain domain: Each data module should be responsible for handling data that represents a certain domain. It can handle many types of data as long as they are related.
  2. Expose the repository as an external API: The public API of a data module should be a repository as they are responsible for exposing the data to the rest of the app.
  3. Hide all implementation details and data sources from the outside: Data sources should only be accessible by repositories from the same module. They remain hidden to the outside. You can enforce this by using Kotlins private or internal visibility keyword.
2_data_modules.png [2_data_modules.png] Figure 1. Sample data modules and their content.

Feature modules

A feature is an isolated part of an app's functionality that usually corresponds to a screen or series of closely related screens, like a sign up or checkout flow. If your app has a bottom bar navigation, its likely that each destination is a feature.

Key Term: "Feature module" is a term that is also used in the Play Feature Delivery describing a module that can be delivered conditionally or downloaded on-demand. However, in the context of this guide a feature module is a module that encapsulates a distinct part of your applications functionality. 2_bottom_bar.png [2_bottom_bar.png] Figure 2. Each tab of this application can be defined as a feature.

Features are associated with screens or destinations in your app. Therefore, they're likely to have an associated UI and ViewModel to handle their logic and state. A single feature doesn't have to be limited to a single view or navigation destination. Feature modules depend on data modules.

2_feature_modules.png [2_feature_modules.png] Figure 3. Sample feature modules and their content.

App modules

App modules are an entry point to the application. They depend on feature modules and usually provide root navigation. A single app module can be compiled to a number of different binaries thanks to build variants.

2_demo_full_dep_graph.png [2_demo_full_dep_graph.png] Figure 4. *Demo* and *Full* product flavor modules dependency graph.

If your app targets multiple device types, such as Android Auto, Wear or TV, define an app module for each one. This helps separate platform-specific dependencies.

2_auto_dep_graph.png [2_auto_dep_graph.png] Figure 5. Android Auto app dependency graph.

Common modules

Common modules, also known as core modules, contain code that other modules frequently use. They reduce redundancy and don't represent any specific layer in an app's architecture. The following are examples of common modules:

Test modules

Test modules are Android modules that are used for testing purposes only. The modules contain test code, test resources, and test dependencies that are only required for running tests and are not needed during the application's runtime. Test modules are created to separate test-specific code from the main application, making the module code easier to manage and maintain.

Use cases for test modules

The following examples showcase situations where implementing test modules can be particularly beneficial:

2_test_modules.png [2_test_modules.png] Figure 6. Test modules can be used to isolate modules that would otherwise be dependent on each other.

Module to module communication

Modules rarely exist in total separation and often rely on other modules and communicate with them. Its important to keep the coupling low even when modules work together and exchange information frequently. Sometimes direct communication between two modules is either not desirable as in the case of architecture constraints. It may also be impossible, such as with cyclic dependencies.

2_mediator.png [2_mediator.png] Figure 7. A direct, two-way communication between modules is impossible due to cyclic dependencies. A mediating module is necessary to coordinate data flow between two other independent modules.

To overcome this problem you can have a third module mediating between two other modules. The mediator module can listen for messages from both of the modules and forward them as needed. In our sample app, the checkout screen needs to know which book to purchase even though the event originated in a separate screen that is part of a different feature. In this case, the mediator is the module that owns the navigation graph (usually an app module). In the example, we use navigation to pass the data from the home feature to the checkout feature using the Navigation component.

navController.navigate("checkout/$bookId")

The checkout destination receives a book id as an argument which it uses to fetch information about the book. You can use the saved state handle to retrieve navigation arguments inside a destination features ViewModel.

class CheckoutViewModel(savedStateHandle: SavedStateHandle, ) : ViewModel() {

   val uiState: StateFlow<CheckoutUiState> =
      savedStateHandle.getStateFlow<String>("bookId", "").map { bookId ->
          // produce UI state calling bookRepository.getBook(bookId)
      }
      
}

You shouldn't pass objects as navigation arguments. Instead, use simple ids that features can use to access and load desired resources from the data layer. This way, you keep the coupling low and don't violate the single source of truth principle.

In the example below, both feature modules depend on the same data module. This makes it possible to minimize the amount of data that the mediator module needs to forward and keeps the coupling between the modules low. Instead of passing objects, modules should exchange primitive IDs and load the resources from a shared data module.

2_shared_data.png [2_shared_data.png] Figure 8. Two feature modules relying on a shared data module.

Dependency inversion

Dependency inversion is when you organize your code such that the abstraction is separate from a concrete implementation.

Modules that rely on the behavior defined in the abstraction module should only depend on the abstraction itself, rather than the specific implementations.

2_di_concept.png [2_di_concept.png] Figure 9. Instead of high level modules depending on low level modules directly, high level and implementation modules depend on the abstraction module.

Example

Imagine a feature module that needs a database to work. The feature module isn't concerned with how the database is implemented, be it a local Room database or a remote Firestore instance. It only needs to store and read the application data.

To achieve this, the feature module depends on the abstraction module rather than on a specific database implementation. This abstraction defines the app's database API. In other words, it sets the rules for how to interact with the database. This allows the feature module to use any database without needing to know its underlying implementation details.

The concrete implementation module provides the actual implementation of the APIs defined in the abstraction module. In order to do that, the implementation module also depends on the abstraction module.

Dependency injection

By now you might be wondering how the feature module is connected with the implementation module. The answer is Dependency Injection. The feature module doesn't directly create the required database instance. Instead, it specifies what dependencies it needs. These dependencies are then supplied externally, usually in the app module.

releaseImplementation(project(":database:impl:firestore"))

debugImplementation(project(":database:impl:room"))

androidTestImplementation(project(":database:impl:mock"))
Note: You can define varying dependencies for distinct build types. For example, the release build could use the Firestore implementation, the debug build could rely on a local Room database, and instrumented tests could employ a mock implementation.

Benefits

The benefits of separating your APIs and their implementations are as follows:

When to separate

It is beneficial to separate your APIs from their implementations in the following cases:

How to implement?

To implement dependency inversion, follow these steps:

  1. Create an abstraction module: This module should contain APIs (interfaces and models) that defines the behavior of your feature.
  2. Create implementation modules: Implementation modules should rely on the API module and implement the behavior of an abstraction. Instead of high level modules depending on low level modules directly, high level and implementation modules depend on the abstraction module. [Instead of high level modules depending on low level modules directly, high level and implementation modules depend on the abstraction module.] Figure 10. Implementation modules depend on abstraction module.
  3. Make high level modules dependent on abstraction modules: Instead of directly depending on a specific implementation, make your modules dependent on abstraction modules. High level modules don't need to know implementation details, they only need the contract (API). High level modules depend on abstractions, not implementation. [High level modules depend on abstractions, not implementation.] Figure 11. High level modules depend on abstractions, not implementation.
  4. Provide implementation module: Finally, you need to provide the actual implementation for your dependencies. The specific implementation depends on your project setup, but the app module is usually a good place to do this. To provide the implementation specify it as a dependency for your selected build variant or a testing source set. App module provides actual implementation. [App module provides actual implementation.] Figure 12. App module provides actual implementation.

General best practices

As mentioned in the beginning there is no single right way of developing a multi-module app. Just like there are many software architectures, there exist numerous ways to modularize an app. Nevertheless, the following general recommendations can help you make your code more readable, maintainable and testable.

Keep your configuration consistent

Every module introduces configuration overhead. If the number of your modules reaches a certain threshold, managing consistent configuration becomes a challenge. For example, its important that modules use dependencies of the same version. If you need to update a large number of modules just to bump a dependency version, it is not only an effort but also a room for potential mistakes. To solve this problem, you can use one of the gradles tools to centralize your configuration:

Expose as little as possible

The public interface of a module should be minimal and expose only the essentials. It shouldn't leak any implementation details outside. Scope everything to the smallest extent possible. Use Kotlins private or internal visibility scope to make the declarations module-private. When declaring dependencies in your module, prefer implementation over api. The latter exposes transitive dependencies to the consumers of your module. Using implementation may improve build time since it reduces the number of modules that need to be rebuilt.

Prefer Kotlin & Java modules

There are three essential types of modules that Android studio supports:

Since Android modules come with overhead, preferably, you'd want to use the Kotlin or Java kind as much as possible.

Content and code samples on this page are subject to the licenses described in the Content License. Java and OpenJDK are trademarks or registered trademarks of Oracle and/or its affiliates.

Last updated 2026-03-05 UTC.

[[["Easy to understand","easyToUnderstand","thumb-up"],["Solved my problem","solvedMyProblem","thumb-up"],["Other","otherUp","thumb-up"]],[["Missing the information I need","missingTheInformationINeed","thumb-down"],["Too complicated / too many steps","tooComplicatedTooManySteps","thumb-down"],["Out of date","outOfDate","thumb-down"],["Samples / code issue","samplesCodeIssue","thumb-down"],["Other","otherDown","thumb-down"]],["Last updated 2026-03-05 UTC."],[],[]]

Web Proxy Viewer  |  New URL  |  Original Page