The new architecture recommendation from Google is changing how we build Android apps. The architecture separates UI and data into distinct layers, with an optional “domain” layer in between. It doesn’t have a specific name like MVVM and is instead referred to as the “new architecture” or “layered architecture.”
The driving principles behind the new architecture are:
- Separation of concerns. Dependencies between components are managed using dependency injection tools.
- Single source of truth and single responsibility principle.
- Unidirectional data flow, exposing streams of data from the data layer.
- Low dependency on Android-related classes in the data layer.
- Separation of data and processing from UI. The UI reflects changes in the data.
The new architecture works neatly with Jetpack Compose, the declarative and reactive UI for Android.
Note: The new architecture is not a strict recommendation; rather, it’s a set of guidelines. The guidelines documentation clearly mentions that the new architecture can be applied to a broad range of apps to allow scalability, testability, and robustness. Developers should treat them as guidelines and adapt them to match the requirements.
In MVVM, we had Activities, ViewModels, Repositories, Models, Utility classes, and connections between them using Android-specific tools like LiveData. In the new architecture, these components are kept in separate layers and the dependency on the underlying framework is minimized.
- We keep our repositories here. Repositories handle data from different sources like network or local storage.
- Repositories expose the data to other layers. The UI layer observes this data and builds the UI.
- This is an optional layer, which handles the business logic.
- It combines multiple repositories and processes the data between them. The UI layer accesses data through the domain layer object.
- This is where Activities, Fragments, Composables, ViewModels, etc. are added.
- It accepts user inputs and other actions and sends them to the domain layer or data layer for processing.
- It is also responsible for handling navigation.
We will build an app that will display the latest links from the popular Hacker News website. Hacker News has a public API documented here. In this example, we will explore the new architecture with Compose.
The app will have the following functionalities:
- Show the top news from Hacker News.
- Show the details of a story, including comments.
- Show the comment replies on a separate screen.
- Allow the user to open the linked article in a browser.
For a better separation of concerns, we can organize the components like this:
- Remote source
- Local source
- Remote source
- Data models for API calls
- Use case
- Fetch Items with time in the “Time Ago” format
- intermediate models (with “Time Ago” data)
- intermediate models (with “Time Ago” data)
- Home screen (list of latest stories)
- Details screen
- MainActivity (with NavHost that hosts other screens/composables)
We will use Hilt for dependency injection.
Let’s go through each layer and see what it does in our case.
The Data Layer
The data layer fetches and stores the data. In the Hacker News API, everything is an “item,” so we only have one repository, named
ItemsRepository. We have local and remote implementations for the data source. The remote source fetches data from the Hacker News API. For keeping things simple, the local source implementation uses memory caching for 10 minutes. After that, the cached data will be cleared. Ideally, this should be a local database implementation.
Other layers will request data from the
ItemsRepository and the repository is responsible for validating the local data, fetching data from the remote API if the local copy is outdated, and caching it for later use. The repository exposes data using Flows.
The data layer has a model
HNItem, which holds the data retrieved from a remote or local source. The API returns the timestamp as Unix epoch. We need to convert this into a displayable format.
The Domain Layer
The domain layer is intended to implement the business logic. It acts as an intermediary between the UI and Data layers. According to official documentation, what we used to keep in the “utils” package previously can go into the domain layer in the new architecture. This also includes anything that is reusable. In our case, we need to convert the Unix epoch coming from the data layer model into a human-readable ‘Time Ago’ format. For example, “a week ago.”
To bring this functionality, I added two use cases:
GetTimeAgoUseCase: This accepts a
Long timestamp and converts it into the “Time Ago” format. We would have kept this in a “util” package earlier.
GetItemsWithTimeAgoUseCase: This fetches the items from the data layer, converts them to a local model named
HNItemWithTimeAgo with the time in “Time Ago” format. They are then exposed to the UI layer using Flows. This data is consumed by the UI layer.
The UI Layer
The UI layer consumes data from the data layer. We used Compose to build the screens and an associated ViewModels to hold the UI State.
The ViewModel will connect to the domain layer or the data layer to fetch the data. Based on the response from these layers, the ViewModel will update the UI State. When the UI State gets updated, the UI will be recomposed to reflect the new state.
We have a
HomeScreen that lists the latest news and an associated
HomeViewModel, which is responsible for fetching the data and updating the
HomeUiState, thus triggering a UI rebuild. The
HomeViewModel doesn’t access the data layer directly; instead, it accesses the domain layer to retrieve the data with the time value in the “Time Ago” format.
ItemDetailsScreen, we have an associated ViewModel
ItemDetailsViewModel, which accesses the Data layer directly and makes use of
GetTimeAgoUseCase to convert the timestamp manually.
You can browse the complete source code here.
Personally, I think the new architecture for Android apps leans toward the upcoming Kotlin Multiplatform for Mobile (KMM) framework, where we can copy the existing data layer to a new KMM project and use the UI layer from the underlying platform (mobile, web, or desktop).