Blog Infos
Author
Published
Topics
, , , ,
Published

This is a series of articles about how to architecture your app that it was inspired by Google Guide to App Architecture and my personal experience.

In the previous article, I covered the Domain layer as the most stable and platform-independent later. Today I am going to cover the purpose of the Data layer. Let’s break it down.

The Data layer is responsible for exposing data to the rest of the app. Manage different data sources and conflict between them.

The Repository

The Data layer is made of Repositories. As I’ve mentioned in the previous chapter, about the Domain layer:

The domain layer should communicate with the data layer only via the interface. We follow the Dependency Inversion Principle (DIP) and place interface in the domain layer and implementation in the data layer.

The Data layer is the implementation of repositories from the Domain layer. The Repository class should represent each different type of data you handle in your app. For example:

interface FaresRepository {
suspend fun getFares(ryderId: String): List<Fare>
}
interface TicketsRepository {
suspend fun buyTicket(ryderId: String, fare: Fare, totalCount: Int)
}
view raw Repositories.kt hosted with ❤ by GitHub
Naming conventions

The Repository classes are named after the data that they’re responsible for.

type of data + DataRepository.

For example, we have an interface TicketRepository and to avoid name collision the implementation of it will be TicketDataRepository .

class FaresDataRepository(
private val faresLocalDataSource: FaresLocalDataSource,
private val faresRemoteDataSource: FaresRemoteDataSource,
private val backgroundDispatcher: CoroutineDispatcher = Dispatchers.IO,
) : FaresRepository { /* ... */ }
Data sources

The Repository manages different sources of data (e.g. local, memory, network). The component responsible for that is the DataSource. It is an abstraction between the repository and source of data such as databases, networks, ShearedPreference, WorkManager, and files.

  • The data source should be responsible for working only with one source of data at a time.
  • The data source should be private for the Data layer and only accessed through the repository.
Naming conventions

Data source classes are named after the data they’re responsible for and the source they use.

type of data + type of source + DataSource.

For the type of data, use Remote or Local to be more generic because implementations can change (e.g. FaresLocalDataSource or FaresRemoteDataSource). To be more specific in case the source is important, use the type of the source (e.g. FaresNetworkDataSource or FaresFileDataSource).

Avoid names based on an implementation detail — for example, UserSQLiteDataSource—because repositories that use that data source shouldn’t know how the data is saved. Following this rule will allow you to change the implementation of the data source (e.g. migrating from SQLite to DataStore) without affecting the layer that calls that source.

class UserRemoteDataSource(
private val userApi: UserApi,
) {
suspend fun getUsers(): List<UserDTO> = userApi.getUsers()
}
interface UserApi {
suspend fun getUsers(): List<UserDTO>
}

The UserApi interface hides the implementation of the network API client. It doesn’t make a difference whether Retrofit or GraphQL backs the interface. Relying on interfaces makes API implementations swappable in your app. It also provides flexibility and allows you to replace dependencies more easily, for example — you can inject fake data source implementations in tests.

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

Jobs

interface UserRetrofitApi: UserApi {
@GET("users")
override suspend fun getUsers(): List<UserDTO>
}

The data source is also a good place to handle an exception when a failure occurs. You shouldn’t expose source-related exceptions to the app, rather map it to the exception that your app knows how to work with.

class UserRemoteDataSource(
private val userApi: UserApi,
) {
suspend fun getUsers(): List<UserDTO> {
try {
return userApi.getUsers()
} catch (e: Exception) {
throw ApiException("Failed to get users", e)
}
}
}

As I mentioned before, the repository is more about managing different sources of data and concurrency.

class FaresDataRepository(
private val faresLocalDataSource: FaresLocalDataSource,
private val faresRemoteDataSource: FaresRemoteDataSource,
private val backgroundDispatcher: CoroutineDispatcher = Dispatchers.IO,
) : FaresRepository {
override suspend fun getFares(ryderId: String): List<Fare> = withContext(backgroundDispatcher) {
if (faresLocalDataSource.isValid()) {
faresLocalDataSource.getData(ryderId).asDomain()
} else {
val fares = faresRemoteDataSource.getData(ryderId)
faresLocalDataSource.setFares(fares)
fares.asDomain()
}
}
}

As you can see in the example above we implemented the strategy of how to manage the local cache and set the priority for the local and remote source of data. The repository can be stateful, for example, you can use Mutex to manage read and write accesses from different threads on the mutable variable. Thus, you need to think about the lifecycle of a repository.

Lifecycle

I recommend you make the repository stateless if it’s possible but at the same time make it singleton (managed through DI) to avoid bugs with instance-duplication of data sources. The same I recommend doing it with DataSource , make them singleton, and manage the cache in the repository.

Concurrency

The Repository is the place where you should decide on which CoroutineDispatcher to execute tasks. It’s good practice to execute different types of tasks on different Dispatcher (or thread pools). for IO operation better to use Dispatcher.IO. Pass dispatcher through class constructor to make testing easier.

Model

The Data layer also has its data models that reflect models from the Domain layer but the representation can be different. The separate data models give us the flexibility to customize it to fit the transport protocol. For example, we use JSON format for communication between server and client and some fields can have different types that are more suitable for JSON protocol. The mapping logic of the domain model to DTO and vice versa should be placed in the Repository class.

@JsonClass(generateAdapter = true)
data class RyderDTO(
@Json(name = "id")
val id: String,
@Json(name = "fares")
val fares: List<FareDTO>,
@Json(name = "subtext")
val subtext: String?,
)
view raw RyderDTO.kt hosted with ❤ by GitHub
  • The data layer should expose and take as input only the domain model.
  • The data model can implement platform-specific ways of serialization such as Parcelable and Serializable.
  • It provides better separation of concerns, for example, members of a team could work individually on the different layers of a feature if the model class is defined beforehand.
  • The data exposed by this layer should be immutable.
Naming conventions

The model classes are named after the data type that they’re responsible for.

type of data + DTO.

For example: RyderDTOFareDTO.

Packaging conventions
data/
├─ local/
│ ├─ dto/
│ │ ├─ FareDTO
│ │ ├─ RyderDTO
│ ├─ FaresLocalDataSource
│ ├─ FaresRemoteDataSource
├─ network/
│ ├─ api/
│ │ ├─ UserNetworkApi // Retrofit implementation of UserApi interface
│ ├─ dto/
│ │ ├─ UserDTO
│ ├─ UserRemoteDataSource // Contains UserApi interface
├─ repository/
│ ├─ FaresDataRepository
│ ├─ RydersDataRepository
│ ├─ TicketsDataRepository
Wrapping up

The data layer is built around repositories implementation defined in the Domain layer. It is responsible for managing different sources of data and strategies for how to use them.

Stay tuned for the next App Architecture topic to cover. Meantime you can read how to map data between layers.

This article is previously published by proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu