In this post, I will share an example of how to manage data relationship for an Android in Kotlin code instead of solely relying on database relationship management by Room database. I will be using the example of a Blog post app. Managing data relationships is one of those things that is still harder than it has to be in programming especially with Android development. Little wonder no-code tooling is growing in popularity where among other things, handling relationship is relatively easy. Consider a simple use case of an Android app that displays blog posts from local storage. The app has the following basic requirement.
- Each post can belong to one category.
- Each category can contain more than one post.
- Each post can have many tags.
- Each tag can be applied to more than one post.
- One author can write each post.
- Each post can have one image/attachment.
- Each attachment can be attached to only one post.
The above requirements represents a combination of classic one to one, one to many and many to many relationship. From a domain standpoint, we only care for a record of type blog post as shown in Example 1
data class BlogPost(
val id: String = "",
val title: String = "",
val content: String = "",
val author: Author = Author(),
val category: Category = Category(),
val tags: List<Tags> = emptyList(),
val attachment: List<Attachment> = emptyList(),
)
Example 1 – Blog Post Data ClassWe can have a UI that displays the blog posts, and all it wants is for us to pass it a list of blog posts to display as shown in Example 2 and not worry about the underlying source of the data or how the relationships between the data is structured.
@Composable
fun BlogPostList(viewModel: BlogPostListViewModel) {
val blogList by viewModel.getBlogPostList().collectAsState(initial = emptyList())
Scaffold() { innerPadding ->
Column(modifier = Modifier.padding(16.dp)) {
LazyColumn(contentPadding = innerPadding) {
items(blogList) { post: BlogPost ->
BlogListCard(blogPost = post)
}
}
}
}
}
Example 2 – Mock UI for showing list of blog postsSample Blog Post Database Schema
For this sample app, we have established that we need the following data classes.
- Author
- Category
- Tag
- Attachment
- Blog Post
The domain usecase of the app requires that we keep records of these domain objects regardless of how we manage their relationships. For this reason, we first need to persit them locally. We can start by modeling each of these object with a data class as shown in Example 3. This captures our requirement.
data class Tags(
val id: String = "",
val name: String = "",
val posts: List<BlogPost> = emptyList(),
val createAt: Long = System.currentTimeMillis()
)
data class Category(
val id: String = "",
val name: String = "",
val posts: List<BlogPost> = emptyList(),
val createAt: Long = System.currentTimeMillis(),
)
data class Author(
val id: String = "",
val name: String = "",
val profilePhotoUrl: String = "",
val posts: List<BlogPost> = emptyList()
)
data class Attachment(
val id: String = "",
val blogId: String = "",
val photoName: String = "",
val photoUrl: String = "",
val mimeType: String = "",
val createAt: Long = System.currentTimeMillis()
)
data class BlogPost(
val id: String = "",
val title: String = "",
val content: String = "",
val author: Author = Author(),
val category: Category = Category(),
val tags: List<Tags> = emptyList(),
val attachment: List<Attachment> = emptyList(),
)
Example 3 – Model classses for mock Blog Post appThese model classes is agnostic of any persistence details. Since I am using Room which decorates its model classes with Room specific annotations, I created different Room specific model classes and then use a mapper class to map between them. These Room model classes have minimal relationship management detail. The relationship will be management with code. Example 4 show the entity classes.
@Entity(tableName = "tags")
data class TagEntity(
@PrimaryKey
val id: String = "",
val name: String = "",
val createAt: Long = System.currentTimeMillis()
)
@Entity(tableName = "category")
data class CategoryEntity(
val id: String = "",
val name: String = "",
val createAt: Long = System.currentTimeMillis(),
)
@Entity(tableName = "author")
data class AuthorEntity(
@PrimaryKey
val id: String = "",
val name: String = "",
val profilePhotoUrl: String = "",
)
@Entity(tableName = "attachments")
data class AttachmentEntity(
val id: String = "",
val journalId: String = ""
val photoName: String = "",
val photoUrl: String = "",
val mimeType: String = "",
val createAt: Long = System.currentTimeMillis()
)
@Entity(tableName = "blog_post")
data class BlogPostEntity(
val id: String = "",
val title: String = "",
val content: String = "",
val categoryId: String = "",
val authorId: String = "",
val createAd: Long = System.currentTimeMillis(),
val modifiedAt: Long = System.currentTimeMillis()
)
Example 4 – Entity classses for mock Blog Post appThese entity classes are sufficient for standard CRUD operations. It does not have relationship management. Room database standard scaffold entities for dao, repository, etc is still required. And another entity class is also required to capture the many to many relationship between tags and blog post as shown in Example 5.
@Entity(tableName = "blog_post_tag_join", primaryKeys = ["blogPostId", "tagId"])
data class BlogPostTagJoin(
val blogPostId: String,
val tagId: String
)
Example 5 – Blog Post/Tags join tableWith this join table, all the tags associated to a blog post can be retrieved with a simple query and then the TagDao can be updated with a query that accepts the list of the tag ids and return the associated tags as shown in Figure 1

Get Blog Post Record with Nested Objects
Now we need to fetch saved blog posts to display in the list. Again, the UI does not care how the lists are modelled or fetched. It just need an observable stream of blog post and an observable instance of a blog post that it can display. Here is an example of a backing ViewModel for the blog post list UI.
@HiltViewModel
class BlogPostListViewModel @Inject constructor(
private val getJournalUseCase: GetJournalUseCase) : ViewModel() {
var currentBlogPost = mutableStateOf(BlogPost())
fun getBlogPostList(): Flow<List<BlogPost>> = getJournalUseCase.getAllBlogPosts()
fun getCurrentBlogPost(blogPostId: String) {
viewModelScope.launch {
getJournalUseCase.getJournalById(blogPostId).collectLatest { blogPost ->
currentBlogPost.value = blogPost
}
}
}
}
Example 6 – Sample ViewModel for mock blog post list UITo support this contract, a mapper is needed to map between the Room database entity class and domain classes. Example 7 shows an interface for the mappper that can be implemented for each of the entity class.
interface Mapper<E, D> {
fun mapToEntity(domain: D) : E
fun mapFromEntity(entity: E): D
fun mapToEntityList(domainList: List<D>): List<E>
fun mapToDomainList(entityList: List<E>): List<D>
}
Example 7 – MapperThe BlogPost Mapper can be extended to support mapping related entities into a domain BlogPost object as shown in Example 8.
fun mapRelatedEntitiesToDomain(
entity: BlogPostEntity,
categoryEntity: CategoryEntity,
authorEntity: AuthorEntity,
attachments: List<AttachmentEntity>,
tags: List<TagEntity>
): BlogPost {
val journal = mapFromEntity(entity)
return journal.copy(
author = AuthorMapper.mapFromEntity(authorEntity),
category = CategoryMapper.mapFromEntity(categoryEntity),
attachments = AttachmentMapper.mapToDomainList(attachments),
tags = TagMapper.mapToDomainList(tags)
)
}
Example 8 – Example of mapping related entities into domain objectWith this mapper in place, a standard retrieve query can be made to the respective repositories to return a flow of the different entities that participate in this relationship. Coroutine FlatMap can be used to chain the queries since some of the queries depend on the result of the preceding query as shown next.
fun getJournalById(blogPostId: String): Flow<BlogPost> {
return repository.getBlogPostByIdFlow(blogPostId)
.flatMapLatest { blogPostEntity: BlogPostEntity ->
attachmentRepository.getAllAttachmentsForBlogPost(blogPostId)
.flatMapLatest { attachmentList: List<AttachmentEntity> ->
categoryRepository.getCategoryByIdFlow(blogPostEntity.categoryId)
.flatMapLatest { categoryEntity: CategoryEntity ->
authorRepository.getAuthorByIdFlow(blogPostEntity.authorId)
.flatMapLatest { authorEntity: AuthorEntity ->
blogTagJoin.getBlogPostRefs(blogPostId)
.flatMapLatest { references: List<BlogPostTagJoin> ->
tagRepository.getAllTags(references.map {
ref -> ref.blogPostId }.toTypedArray())
.map { tags: List<TagEntity> ->
BlogPostMapper
.mapRelatedEntitiesToDomain(
blogPostEntity,
categoryEntity,
authorEntity,
attachmentList,
tags
)
}
}
}
}
}
}
}
Example 9 – Chaining Coroutine Flow queries to return a single observableThe list of blogs posts can also be implemented in similar way by implementing a mapper that returns the list. This can then be called like so.
fun getAllBlogPosts(): Flow<List<BlogPost>> {
return repository.getAllBlogPosts().mapLatest {entityList ->
mapListOfEntitiesToDomainList(entityList)
}
}
Example 10 – Fetching list of blog postSummary
As with most things in this craft, this is an opinionated approach. While I can write SQL statement to create and manage data relationship, I prefer to keep data persistence implementation constrained to CRUD usecases. This way, business rule and domain objects can change without requiring database migration. Not to mention the fact that SQL statements are error prone. To see the skeleton implementation of this data layer checkout this Github repository. Note that it does not contain Compose UI.