Some Best Practices for Android App Architecture

Kayvan Kaseb
Software Development
8 min readApr 22, 2022
The picture is provided by Unsplash

As a matter of fact, an architecture is the foremost abstraction of a system that choose particular details and suppresses others. In addition, it would be extremely significant to define an architecture for Android apps because there have been a remarkable growth in size in recent years. Thus, Android app architecture plays a vital role in the following issues: scaling the app, enhancing the app’s robustness, and making the app easier to test. Recently, some guidelines and best-practices have been recommended by Google to develop high-quality Android apps with effective architecture in practice. This article will provide you with some best-practices in modern Android app architecture briefly.

Introduction and Overview

Fundamentally, there are many definitions of software architecture; however, architecture could be defined as an abstraction of a system. This means that architecture eliminates specific information about elements that is not helpful for reasoning about the system. As a result, an architecture is the foremost abstraction of a system that choose particular details and suppresses others.

A good architecture is important, otherwise it becomes slower and more expensive to add new capabilities in the future

— Martin Fowler

Furthermore, enterprise software architecture always evolves with advanced architectural styles in order to find better patterns to build software in a fast and reliable way. In particular, it would be extremely important to define an architecture for Android apps because there have been a remarkable growth in size in recent years. So, Android app architecture plays an essential role in the following issues: scaling the app, enhancing the app’s robustness, and making the app easier to test.

Eventually, nowadays, some guidelines and best-practices have been suggested by Google to develop high-quality Android apps with efficient architecture in practice. In short, Google has offered that each Android app should include two layers at least: First, the UI layer that shows app data on the screen. Second, the data layer that consists of the business logic of your app and exposes application data. Nevertheless, to ease and reuse the interactions between the UI and data layers, you can be able to add an optional layer called the domain layer.

The picture is provided by Google documents

This essay will mention some best-practices in modern Android app architecture briefly.

The UI layer

  1. Fundamentally, the main role of the UI layer or presentation layer is to show the application data on screen. Moreover, it serves as the central point of user interaction.
  2. Naming Convention: functionality + UiState.
  3. Generally, the UI layer includes two issues: UI elements + UI state = UI.
  4. The UI elements render the data on the screen. Thus, you can be able to create these elements via Views or Jetpack Compose functions.
  5. The UI layer has to accomplish the following steps successfully: 1) Transforming App data to UI specific app data. 2) Updating UI elements to reflect UI specific data. 3) Processing user input events that lead to UI changes. 4) Repeating all the above steps as long as necessary.
  6. The UI state definition should be immutable because this can be useful for maintaining the focus of UI layer on main purpose: reading the state and updating its related UI elements.
  7. You should never modify the UI state in the UI directly unless the UI itself is the one and only source of its data due to dealing with inconsistencies and bugs.
  8. Essentially, the business logic can be classified into two main category in this area: First, in terms of what to do with state changes, you should handle this kind of business logic in the domain and data layers. Second, according to how to display state changes, the UI layer should manage this UI behavior logic, such as implementing navigation logic or showing messages to the user.
  9. If your UI logic contains some types like Context, you should put it in the UI elements, not in the ViewModel.
  10. In some special cases, because of complexity of UI, you can have an opportunity to create a simple class as a state holder for these types in order to follow separation of concerns as a guideline in your code.
  11. Even though there are many ways to represent the interaction between the UI and its state producer, a unidirectional data flow (UDF) could be a proper pattern to display this interaction. This leads to data consistency, testability, and maintainability.
  12. The question is that how to know the UI state has been changed? The answer is observable data.
  13. The UI state should be exposed in an observables data holder, like StateFlow or LiveData.
  14. Oftentimes, state holders like ViewModels live much longer than the UIs that observe them. So, you should make sure that they are not observing UI state when they cannot be seen.
  15. The ViewModel type is the recommended implementation for the management of screen-level UI state with access to the data layer.
  16. Consuming UI state is performed with the terminal operator on the observable data type. For instance, if you use LiveData, this is the observe() method. Also, if you use the Kotlin flows, this is the collect() method.
  17. While observing and consuming UI state in the UI, you should not ignore the lifecycle of the UI.
  18. A UI state object should handle states that are related to each other.
  19. In Jetpack Compose applications, you can be able to use Compose’s observable State APIs, such as mutableStateOf or snapshotFlow for the exposure of UI state.
The picture is provided by Google documents

The Domain Layer

  1. Fundamentally, the domain layer is in charge of encapsulating complicated business logic. Otherwise, simple business logic that is reused by multiple ViewModels.
  2. The domain layer is not in charge of how data is showed, this is the main job of the UI layer, and it is not responsible for storing or retrieving data, that is the central job of the data layer.
  3. There are some advantages could be mentioned for using domain layer as follows: 1) It makes easier to achieve the single responsibility principle in your code. 2) It boosts readability in classes that use domain layer classes. 3) It enhances the testability of the Android app by this modularization approach.
  4. The domain layer contains some classes that are typically called use cases or interactors. Each use case should have responsibility over a single task, and should not include mutable data.
  5. Naming convention for this issue as follows: verb in present tense + noun/what (optional) + UseCase
  6. If you require to cache data, this logic might be better placed in the data layer.
  7. Use cases can be dependent on lower layer, such as repositories in the data layer and on other use cases, but they should not be dependent on higher layers, like ViewModels.
  8. Use cases consist of reusable logic. Therefore, they can also be used by other use cases, and it would be ideally normal to have multiple levels of use cases in the domain layer.
  9. Use cases perform just only one task.
  10. When you pass your use case as a dependency, like constructing a ViewModel, you can call your use case like a normal function.
  11. Use cases do not have their own life cycle, but they are scoped to the class that uses them (scoped to the caller).
  12. Use cases from the domain layer has to be main-safe. This means they have to be safe to call from the main thread. Besides, if a use case is performing a long-running blocking operation, you should move it to a background thread.
  13. If you have logic that is used by multiple ViewModels, you should put this logic inside a use case. In other words, you just only require to change code in one centralized and encapsulated place.
  14. The second typical task for use cases is combining data from multiple repositories. This avoids involving our ViewModel with logic, and builds an easily testable unit as well. So, it can be reused wherever the functionality is needed.
  15. The use case takes the repositories as dependencies and the dispatcher to be used for background work.
  16. By restricting your approach in Android app architecture, you can force everything to be done through the domain layer. This can lead to some benefits:1) It avoids domain layer logic being bypassed. 2) It makes unit testing ViewModels easier. However, it can add more complexity to your code.

The Data Layer

  1. The main role of the data layer is holding, handling, and providing access to the app data.
  2. Naming convention: type of data + Repository.
  3. Basically, the data layer consists of application data and business logic.
  4. The data layer specifies how an app data must be created, stored, and changed. In fact, the data layer is made of repositories that each can interact with zero to some data sources.
  5. They should have the responsibility of working just only with one source of data, which generally holds a unit of business logic.
  6. Data sources can be defined as remote or local.
  7. Repositories centralize typical data access functionality, provide better maintainability, and decouple the infrastructure or technology utilized to access databases from another layer.
  8. You should create a repository class for each different type of data that you will manage in your Android app.
  9. Other layers in the hierarchy of app architecture should not access data sources directly. Thus, the entry point to the data layer is always the repository classes in your code.
  10. Initially, a repository defines data operations.
  11. You can be notified of data changes over time by exposing a data stream, like using Kotlin’s flow.
  12. The data exposed from the repository should always be the one, which coming from the source of truth in a direct way.
  13. If there is a conflict between the data in your local database and the server, the repository should identify and fix it efficiently.
  14. One of the main concepts is that he data exposed by the data layer should be immutable; therefore, all the classes cannot manipulate or tamper with it. Another benefit of immutable data is that it can be safely handled by multiple threads.
  15. Obviously, repository can be built on multiple data sources, but you might want to have multiple levels of repositories in some situations because of having more complex business requirements.
  16. One solution for handling errors is to simply allow exceptions propagate with suspend functions. You can be able to use common try/catch blocks from the UI or the domain layer; otherwise, if you are using flows you can use the catch operator.
  17. In any condition, you must address errors effectively that they will happen in data layer.
The picture is provided by Google documents

In conclusion

In fact, Android app architecture plays an essential role in the following issues: scaling the app, improving the app’s robustness, and making the app easier to test. Nowadays, some guidelines and best-practices have been offered by Google to develop high-quality Android apps with effective architecture in practice. In a word, Google has recommended that each Android app should contain two layers at least: First, the UI layer and the data layer. However, to ease and reuse the interactions between the UI and data layers, you can be able to add an optional layer called the domain layer. This article indicated some best-practices in modern Android app architecture based on Google resources briefly.

--

--

Kayvan Kaseb
Software Development

Senior Android Developer, Technical Writer, Researcher, Artist, Founder of PURE SOFTWARE YAZILIM LİMİTED ŞİRKETİ https://www.linkedin.com/in/kayvan-kaseb