Architecture
Data Layer
Let's dive a little deeper into the Data Layer.
The most basic part of the Data Layer is a Data Source. This is where you access your data. It could be a local or remote database, a web service, a file or another place to store your data. The data in this layer is usually persisted so it can be accessed across runs of the application.
Sometimes your application may use more than one data source. For example, if your application managed contacts, they might be stored in a server for access across devices. To reduce the user's network use, you may also have a local database that stores any contacts that have been previously accessed.
So how do you decide which data source to use?
Add a repository module. A repository can act as a switch between data sources. If the repository is asked for a contact, it checks to see if it's in the local-database data source. If so, it just returns it; no network communication needed! If not, it grabs the contact from the network, stores it in the local database, then returns it. (We're ignoring data that's been changed on the server in this example, but there are other technologies that make that easy to manage.)
flowchart LR
other[User Interface/Domain Layer] --> repo
ds1 --> db[(Database)]
ds2 --> file(((File)))
ds3 --> ws(((Web Service)))
subgraph Data Layer
direction LR
repo[Repository] --> ds1[Data Source 1]
repo[Repository] --> ds2[Data Source 2]
repo[Repository] --> ds3[Data Source 3]
end
(Arrows represent dependencies)
The repository can also perform another useful function - convert data that's used directly by a data source to data that can be returned to other layers. This conversion can
- limit which data is available to other layers (hiding data that you don't want to expose)
- add new data that's derived from a data source or pulled in from other sources
- change the accessibility of data, commonly making it immutable
- return objects that implement interfaces required by other layers
One of the most important things we'll learn about our User-Interface Layer is that immutable data makes everything more reliable and can help frameworks like Jetpack Compose optimize what needs to be refreshed. Immutable data stops its users for directly modifying it; they must use other functions to make changes, functions that will then know that changes are being made. This allows for easy enforcement of Unidirectional Data Flow, which we'll talk about later.
The repository copies the data from the actual data objects (often known as "entities") and creates Data Transfer Objects (DTOs) to carry that data. DTOs are often immutable, and can also restrict which data is visible or enhance the data with derived properties. DTOs may be a simple wrapper (an "Adapter" in design-pattern parlance), or an entirely-separate object that holds a copy of the data.
Using a repository helps abstract the way the data is accessed outside the Data Layer. Depending on your level of abstraction, this may result in better isolation between the layers. Changes to how you store data become less likely to force changes outside the Data Layer.
Note
"Less likely" depends on the amount of abstraction used when exposing data from the data layer, and this is a tradeoff. Higher abstraction requires more types (interfaces and/or classes) to be defined and maintained and reduces required external changes. Lower abstraction (such as directly passing data that's managed by your database) is less to develop and maintain, but requires more to change outside the layer when data-later internals change.
Think about how likely change is in your application. Is it likely you'll switch data sources? Is it likely the app will only be an Android application, or might you want to use parts of it in a desktop or web application? (Eventually, I think that Kotlin Multiplatform [KMP] will also allow easy reuse of much of your code on iOS as well.)