
Overview of Android Application Architecture
Benefits of Using Application Architecture
Typically, an Android application comprises various components such as activities, fragments, services, content providers, and broadcast receivers. How can we organize, manage, and connect these components to create an outstanding application? The answer lies in using application architectures.
Implementing a robust architecture in an Android application brings numerous benefits to the project and your team:
- Improved Maintainability, Quality, and Overall Robustness: A well-structured architecture enhances the application’s maintainability and quality, making it more robust overall.
- Scalability: It allows the application to scale effectively. Adding more team members or teams to the project can be done on the same codebase with minimal errors and conflicts.
- Increased Efficiency: Consistent architecture speeds up the project and enhances efficiency. New team members can quickly integrate into the workflow with less time and effort spent on understanding the project.
- Ease of Testing: A good application architecture promotes the use of simpler structures, making the application easier to test.
- Systematic Error Detection: Errors can be systematically identified with clearly defined processes.
From a user perspective, the architecture used in an application also directly impacts their experience, resulting in a more stable application with more features contributed by multiple members and teams. However, mastering a specific application architecture requires investing time and effort to learn and apply it correctly.
General Principles in Android Application Architecture
As Android applications become heavier and larger, defining an architecture that allows the application to scale, increase robustness, and improve testability and error detection is essential.
We should not use the main application components mentioned above to store data and application states. Therefore, designing an architecture that meets the maximum standards recommended by many reputable organizations is crucial.
An application architecture defines the boundaries between different parts of the application and the responsibilities of each part. To achieve this, follow these specific rules when designing the application architecture:
Separation of Concerns
- Separation of Concerns: This is one of the most critical rules to follow.
- Avoid Writing All Code in an Activity or Fragment: These UI management classes should only handle UI logic and interactions sent from the operating system.
- Keep UI Controllers Lean: By keeping UI controllers as lean as possible, many issues related to component lifecycle can be avoided, and the testability of these classes is improved.
- Minimize Dependencies on Activities and Fragments: To meet user experience and create a manageable application, minimize dependencies on activities and fragments.
Drive UI from Data Models
- Control UI from Data Models: Control UI state through data models in use, prioritizing persistent models.
- Data Models: Represent the application’s data, often as entity classes, models, or data classes (Kotlin). They should be independent of UI and other application components.
- Persistent Data Models: Ideal because:
- Users do not lose data if the OS kills the application to free up resources.
- The application continues to function in cases where the network connection is unavailable or unstable.
- Building the application architecture based on data models makes the application more robust and testable.
Single Source of Truth (SSOT)
- Single Source of Truth (SSOT): Each new data type should have a designated owner.
- SSOT Benefits:
- Centralizes all changes to a specific data type in one place.
- Protects data so other types cannot tamper with it.
- Simplifies tracking and monitoring data changes, leading to quicker error detection.
In offline-first applications, SSOT is often the database. In other cases, it can be a ViewModel or UI.
Unidirectional Data Flow (UDF)
- Unidirectional Data Flow (UDF): Typically accompanies the SSOT rule. In UDF, the state of data flows only in one direction, while events causing changes flow in the opposite direction.
- Data Flow Example:
- Data flows from data sources to UI.
- User interaction events, like button presses, flow from UI to SSOT, where application data is modified and published to the UI through a read-only type.
This design pattern ensures better data consistency, fewer errors, easier debugging, and all the benefits of SSOT.
Recommended Application Architecture
From the analysis in the sections above, every Android application should include at least two layers:
- UI Layer: Displays the application data on the screen.
- Data Layer: Contains business logic and provides data to the application.
Optionally, a Domain Layer can be added to simplify and reuse interactions between the UI and data layers.
Arrows in the diagram indicate the direction of dependencies between classes. UI classes always depend on domain classes, and domain classes depend on data classes.
UI Layer (Presentation Layer)
- Primary Responsibility: Displays application data on the screen.
- Data Changes: Whenever data changes, whether due to user interaction or internal inputs like network responses, the UI layer should update to reflect the data changes.
- UI Layer Components:
- UI Elements: Render data on the screen, including Views or Jetpack Compose components.
- State Holders: Such as ViewModel, store data, provide data to UI, and handle related logic.
Domain Layer
- Optional Layer: Between the UI and data layers.
- Encapsulates Complex Business Logic: Or simplifies business logic reused by multiple ViewModels.
- Use Cases: Known as use cases or interactors, each responsible for a specific function.
Data Layer
- Contains Business Logic: Provides values to the application and defines how an application creates, stores, and modifies data.
- Repositories: Consist of repositories that may include zero to many data sources.
- Example: Create a
MoviesRepositoryfor movie-related data, aPaymentRepositoryfor payment-related data, and anOrdersRepositoryfor order-related data.
Repositories perform functions such as:
- Providing data to the rest of the application.
- Centralizing data changes.
- Handling conflicts between multiple data sources.
- Abstracting the origin of data from the rest of the application.
- Containing business logic.
Data sources are responsible for working with only one data source, which could be a file, network resource, or local database, and serve as the bridge between the application and the system when performing data operations.
Managing Dependencies Between Components
Classes in our application often rely on other classes to complete functions correctly. We have two main ways to manage dependencies:
- Dependency Injection (DI): Allows classes to define their dependencies without building dependencies from scratch. Other classes provide these dependencies when the application operates.
- Service Locator: Provides a registry where classes can manage dependencies instead of creating them.
These design patterns allow for scalable code by providing clear patterns for managing dependencies without code duplication or added complexity. They also facilitate switching between testing and product deployment quickly.
Recommended Actions
While the following actions are not mandatory, they are recommended to create a strong, testable, and maintainable codebase over time.
- Avoid Storing Data in Application Components:
- Do not design application entry points like activities, broadcasts, or services as data sources.
- Instead, they should navigate with other components to access related datasets.
- Applications often have short lifespans depending on user interaction and system health.
- Reduce Dependencies in Android Classes:
- Application components should only include classes based on the Android framework, such as Context or Toast.
- Other classes should be abstracted from these components, increasing testability and reducing interconnections.
- Define Clear Boundaries of Responsibility:
- Do not scatter code for loading data from network connections across multiple classes or packages.
- Similarly, do not define unrelated responsibilities within the same class.
- Minimize Module Exposure:
- Avoid creating shortcuts to access detailed implementation from a module. This prevents technical debt as the project scales.
- Focus on Unique Core Values:
- Avoid reinventing the wheel by repeatedly writing the same code.
- Focus time and energy on what makes your application unique, letting Jetpack libraries and recommended libraries handle boilerplate code.
- Enable Independent Testing of Each Part:
- Define a good API for retrieving data from network connections, facilitating testing of modules storing data in the local database.
- Avoid merging logic from two modules into one or distributing the implementation of a function across the entire project, as it makes testing difficult and potentially ineffective.
- Ensure Data Types Are Responsible for Their Concurrency Rules:
- If a type performs a blocking operation for a long time, it should handle transferring the calculation to the correct thread.
- The specific type should know what calculation is being performed and which thread it should run on.
- Types should meet the requirement of being safe for the main thread, ensuring that calls from the main thread are safe and do not cause blocking.
- Maintain Up-to-Date and Relevant Data:
- Users should enjoy application functions even when the device is offline.
- Not all users prefer maintaining a continuous, high-speed online connection.
- Even if they do, they may encounter poor connectivity in areas with high connection density.







-
Tagged flutter, mobile-dev, react-native