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.

What architecture to choose for the project? A common question for many engineers and often the choice is to follow Clean Architecture principles. Put the code in different layers, usually, it’s Data, Domain, and Presentation. Each of the layers has its components logic, and data models. One of the main ideas of different layers is to have a dependency hierarchy and in the context of that today, I’ll show you how to map data to transfer through the layers.

Let’s assume we have the dependency hierarchy as in the picture below and zero in on each layer of data mapping.

Naming conventions

Before we shed the light on each layer, let’s agree on naming conventions. The mapping functions are named after the layer data type. We reserve prefixes for specific functions that map data model representation between layers. The convention is as follows:

as + layer data type.

For example, we have CategoryDTOCategoryCategoryModel for Data, Domain, and Presentation layers respectably. The mapping function’s names will be asDTOasDomainasPresentation respectively.

If you need to transform one type to another we reserve prefix to. The convention is as follows:

to + data type.

For example, CategoryModel has adsCount property that we want to map to UI string representation, and create the toAdsCountFormat function.

fun Int?.toAdsCountFormat(): String? =
if (this == null) null else FormatUtils.getDecimalNumber(this)

The difference between prefixes as and to is that use as for name when we map the same model that has representation in each layer and to for function to map one type to complete a different one.

Data layer

This layer knows about the Domain and is responsible for mapping the Domain model to DTO and DTO to the Domain when we expose data. The DTO model usage should be encapsulated in the Data layer only.

@JsonClass(generateAdapter = true)
data class CategoryDTO(
@Json(name = "id") val id: Long,
@Json(name = "image") val image: String? = null,
@Json(name = "children") val children: List<CategoryDTO>,
@Json(name = "name") val name: String,
@Json(name = "ads_count") val adsCount: Int? = null,
@Json(name = "feed_type") val feedType: FeedType? = null,
) {
enum class FeedType(val id: String) : Serializable {
@Json(name = "map")
MAP("map"),
@Json(name = "default")
DEFAULT("default");
companion object {
fun from(id: String): FeedType? {
return when (id) {
MAP.id -> MAP
DEFAULT.id -> DEFAULT
else -> null
}
}
}
}
}
view raw CategoryDTO.kt hosted with ❤ by GitHub

As you can see DTO models are used for parsing JSON and we shouldn’t expose this logic out of the Data layer, it allows us to align the DTO model with the server or local cache data structure.

Put mapping functions in the same Kotlin file as the model declaration.

fun CategoryDTO.asDomain() = Category(
id = id,
image = image,
name = name,
children = children.asDomain(),
adsCount = adsCount,
feedType = feedType.asDomain(),
)
fun Category.asDTO() = CategoryDTO(
id = id,
image = image,
name = name,
children = children.asDTO(),
adsCount = adsCount,
feedType = feedType.asDTO(),
)
fun List<Category>.asDTO(): List<CategoryDTO> {
return map { it.asDTO() }
}
fun List<CategoryDTO>.asDomain(): List<Category> {
return map { it.asDomain() }
}
fun CategoryDTO.FeedType?.asDomain() = when (this) {
CategoryDTO.FeedType.MAP -> Category.FeedType.MAP
CategoryDTO.FeedType.DEFAULT -> Category.FeedType.DEFAULT
else -> Category.FeedType.DEFAULT
}
fun Category.FeedType.asDTO() = when (this) {
Category.FeedType.MAP -> CategoryDTO.FeedType.MAP
Category.FeedType.DEFAULT -> CategoryDTO.FeedType.DEFAULT
}
Domain layer

The Domain models shouldn’t contain any layer mapping function, because the Domain doesn’t know about Data and Presentation layers at all. The model on this layer shouldn’t be Parcalable or any other platform-dependent code. This layer is the source of truth in the project and should be platform-independent, so we can reuse it for different platforms (e.g. Kotlin multiplatform project).

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

No results found.

Jobs

data class Category(
val categoryId: Long,
val image: String?,
val children: List<Category>,
val name: String,
val adsCount: Int?,
val feedType: FeedType,
) {
val hasChildren: Boolean
get() = children.isNotEmpty()
val hasAds: Boolean
get() = adsCount != null && adsCount > 0
enum class FeedType(val id: String) {
MAP("map"),
DEFAULT("default");
}
}
view raw Category.kt hosted with ❤ by GitHub
Presentation layer

The Presentation layer knows about the Domain but doesn’t know about the Data layer. The logic for mapping the Domain models into Presentation and vice versa should be in the Presentation layer. Models in this layer can be platform-dependent (e.g. Parcelable) and be more UI-specific.

@Parcelize
data class CategoryModel(
val categoryId: Long = ROOT_CATEGORY_ID,
val name: String = "",
val children: List<CategoryModel> = emptyList(),
val image: String? = null,
val adsCount: Int? = null,
val feedType: FeedType = FeedType.DEFAULT,
) : Parcelable {
companion object {
internal const val ROOT_CATEGORY_ID = 1L
}
@IgnoredOnParcel
val isRoot: Boolean
get() = categoryId == ROOT_CATEGORY_ID
@IgnoredOnParcel
val hasChildren: Boolean
get() = children.isNotEmpty()
@IgnoredOnParcel
val hasAds: Boolean
get() = adsCount != null && adsCount > 0
enum class FeedType(val id: String) {
MAP("map"),
DEFAULT("default")
}
}

Mapping functions:

fun Category.asPresentation() = CategoryModel(
categoryId = categoryId,
image = image,
name = name,
children = children.asPresentation(),
adsCount = adsCount,
feedType = feedType.asPresentation(),
)
fun Map<CategoryId, List<Category>>.asPresentation(): Map<CategoryIdModel, List<CategoryModel>> =
mapKeys { it.key.asPresentation() }.mapValues { it.value.asPresentation() }
fun List<Category>.asPresentation(): List<CategoryModel> = map { it.asPresentation() }
fun Category.FeedType.asPresentation() = when (this) {
Category.FeedType.MAP -> CategoryModel.FeedType.MAP
else -> CategoryModel.FeedType.DEFAULT
}
fun CategoryModel.asDomain() = Category(
categoryId = categoryId,
image = image,
name = name,
children = children.asDomain(),
adsCount = adsCount,
feedType = feedType.asDomain(),
)
fun Map<CategoryIdModel, List<CategoryModel>>.asDomain(): Map<CategoryId, List<Category>> =
mapKeys { it.key.asDomain() }.mapValues { it.value.asDomain() }
fun List<CategoryModel>.asDomain(): List<Category> = map { it.asDomain() }
fun CategoryModel.FeedType?.asDomain() = when (this) {
CategoryModel.FeedType.MAP -> Category.FeedType.MAP
else -> Category.FeedType.DEFAULT
}
DTO to Domain Model mapping test

The mapping logic also needs to be coveted by JUnit tests

@Test
fun `On asDomain call should return domain model`() {
val categoryDTO = randomCategoryDTO()
val domainModel = categoryDTO.asDomain()
assertEquals(categoryDTO.id, domainModel.categoryId.id)
assertEquals(categoryDTO.image, domainModel.image)
assertEquals(categoryDTO.name, domainModel.name)
assertEquals(categoryDTO.children.size, domainModel.children.size)
assertEquals(categoryDTO.adsCount, domainModel.adsCount)
assertEquals(categoryDTO.feedType?.asDomain(), domainModel.feedType)
}

Today we have covered a few changes that can add more structure to your project and improve code style.

Stay tuned for the next App Architecture topic to cover.

Thanks for reading.

This article is previously published on 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
Menu