Skip to content

Room

The Repository

A Repository is an optional abstraction at the top of the Data Layer. It's often used to

  • Expose data from multiple data sources
  • Cache data from a remote data source (such as a web service) in a local data source (such as a database or file)
  • Convert data into Data Transfer Objects that expose a more restricted view of the data

In this class, we'll be using it to create Data Transfer Objects. If you're interested in other uses of a Repository, search online for terms such as "Android Repository Cache".

But first, where does the Repository go? It's part of the Data Layer, but by smart use of Modules in your application, you can take advantage of dependencies between modules to prevent other layers from accidentally accessing data directly.

flowchart LR
    subgraph User Interface Layer
    ui[User Interface Module\nView Model\nUser interface]
    end
    ui --> repo
    subgraph Data Layer
    repo[Repository Module\nRepository\nData Transfer Objects]
    ds[Data Source Module\nEntities\nDAO]
    repo --> ds
    end

The separate Repository Module prevents the User Interface Module from directly accessing anything in the Data Source Module (assuming you set up module dependencies in your build to restrict that). It's a great way to hide details as well as make the data we use in the user interface immutable, which is great for the Jetpack Compose UI that we'll be creating.

We'll see how these modules are set up when we're walking through the example code.

Repository Code

So what does the code in the Repository module look like?

First, let's define a Data Transfer Object for our Person:

data class PersonDto(
    val id: String,
    val name: String,
    val age: Int,
    val ssn: String,
) 

Note that all of the properties here are val properties; we can only read them; we cannot change them. We can also define a couple of helper extension functions

internal fun Person.toDto() =
    PersonDto(
        id = id,
        name = name,
        age = age,
        ssn = ssn
    )

internal fun PersonDto.toEntity() =
    Person(
        id = id,
        name = name,
        age = age,
        ssn = ssn
    )

Kotlin extension functions make it look like we're defining new functions on existing types. Here we add a toDto function to our Person entity, and a toEntity function to our PersonDto.

Note that both functions are marked internal. This restricts their use to inside of the module defining them. We're only doing the transformation inside the Repository module. Marking them internal allows us to use them anywhere within the module, no matter which package.

Next, we define an interface for the Repository:

interface PersonRepository {
    val peopleFlow: Flow<List<PersonDto>>
    suspend fun insert(vararg people: PersonDto)
    suspend fun update(vararg people: PersonDto)
    suspend fun delete(vararg people: PersonDto)
}

Note that this interface only uses the PersonDto, not the Person entity. We're only exposing the DTO.

We can (and will) have multiple implementations of this interface. We'll start with one that talks to a Room database, and later in the course we'll implement it by talking to a web service.

class PersonDatabaseRepository(
    context: Context
): PersonRepository {
    private val dao =
        Room.databaseBuilder(
            context,
            PersonDatabase::class.java,
            "PEOPLE"
        )
        .build()
        .dao

    ...
}

To create a Room database instance, we need an Android Context. The Context (typically an Application or Activity instance) gives us access to details about the application, such as where our database files are stored.

We pass that to the PersonDatabaseRepository, create a database instance and grab the DAO from it.

Next, we expose a Flow<List<PersonDto>>. To do this, we'll take advantage of the map operator on Flow. map creates a new Flow by collecting objects from a Flow and transforming each item into something else.

class PersonDatabaseRepository(
    context: Context
): PersonRepository {
    private val dao = ...

    override val peopleFlow =
        dao.getPeople()
            .map { people ->                // AAA
                people.map { it.toDto() }   // BBB
            }

    ...    
}

The first map call creates that new Flow. Whenever we get a new value, the List<Person>, we pass it to the lambda starting on line AAA. That list is represented by lambda parameter people.

Using a map operator against that list (line BBB), we create a new list by converting each Person into a PersonDto.

Anyone collecting peopleFlow will get a List<PersonDto> that contains a read-only copy of the Person entity data.

The one-shot functions are all implemented similarly, so we'll just talk through the insert function.

class PersonDatabaseRepository(
    context: Context
): PersonRepository {
    private val dao = ...

    ...

    override suspend fun insert(vararg people: PersonDto) =
        dao.insert(
            *people                     // AAA
                .map { it.toEntity() }  // BBB
                .toTypedArray()         // CCC
        )

    override suspend fun update(vararg people: PersonDto) = ...
        // similar to insert

    override suspend fun delete(vararg people: PersonDto) = ...
        // similar to insert
}

The caller will pass in a varying-length argument list of PersonDto instances. This means they can pass any number of PersonDtos into insert, separarted by commas. For example

repository.insert(person1)
repository.insert(person1, person2, person3)

are valid calls to insert.

Inside insert, the people parameter is an Array<PersonDto>. We need to convert each PersonDto to a Person entity.

Line BBB converts people into a List<Person>.

But now we need to pass those Person instances into a varying-length, comma-separated argument list. Java would allow you to just pass an array, but that caused some ambiguity problems. Kotlin removed the ambiguity, but it had to make things a little more complex in the process.

We need to "spread" the values out as a comma-separated list. That's what the * does on line AAA. * is called the "spread operator" in this context. But it only works against an Array of values, so we need to call toTypedArray on line CCC to convert our new List<Person> into an Array<Person>.

Once spread, we pass the values into the DAO's insert function.

Implement the update and delete functions similarly, and we now have a repository that isolates the Room data from the User Interface Layer.