This is a snippet from my book “Kotlin and Android Development featuring Jetpack” from the Pragmatic Bookshelf, covering how to build a Repository class for a native (Kotlin-based) Android app.

Here, we’re building an Android app based on a game called “Penny Drop”, which has each player rolling a single six-sided die and placing pennies in slots based on what they rolled. We’ve already created a Room-backed database, so now we need a way to access that database from various locations in the app.

Create a Repository Class

First off, a repository isn’t a specific Room-related class but rather a recommended Jetpack convention. The advantage of using a repository is that the ViewModel classes have a single location for all data access, no matter the source of that data. This means that if we were to have web service calls along with our database calls, the front-end components would go to the same place for both and not even care about which was which. Having a repository separates out how you get data with how you use the data.

In our case, having a repository is admittedly a bit overkill since we only have a single data source and as you’ll see, the functions in our repository will just be pass-throughs to PennyDropDao functions. Still, it’s good practice to have the repository in place once you move onto more complicated apps, like the Android Baseball League app.

To start, create the PennyDropRepository class in the data package and send in an instance of PennyDropDao as a private value.

class PennyDropRepository(private val pennyDropDao: PennyDropDao) { ... }

The PennyDropRepository class will have four functions to start: getCurrentGameWithPlayers(), getCurrentGameStatuses(), startGame(), and updateGame(). This highlights another advantage of a repository: We can limit the functions that are shown to the rest of the app while allowing the DAO to have extra internal functionality. While in most cases we could just make those functions protected, this would restrict us from using them in other DB-related activity, such as during RoomDatabase initialization.

As I mentioned before, each of these functions will be calling the corresponding function in PennyDropDao. Also, the latter two functions will be marked with the suspend keyword as they’re modifying the database. The entire PennyDropRepository looks like this:

class PennyDropRepository(private val pennyDropDao: PennyDropDao) {
	fun getCurrentGameWithPlayers() = 
		pennyDropDao.getCurrentGameWithPlayers()

	fun getCurrentGameStatuses() =
		pennyDropDao.getCurrentGameStatuses()

	suspend fun startGame(players: List<Player>) =
		pennyDropDao.startGame(players)

	suspend fun updateGameAndStatuses(
		game: Game,
		statuses: List<GameStatus>
	) = pennyDropDao.updateGameAndStatuses(game, statuses)
}

The PennyDropRepository is now complete, but unfortunately none of the DAO functions currently exist. Let’s head back over to PennyDropDao and add them in.

Add PennyDropDao Functions

We’ve got four functions to add and we’re going to go right in order. First up, getCurrentGameWithPlayers(). This function is similar to what we saw earlier (in particular the getPlayer() function) but it also includes the @Transaction annotation. This annotation tells Room that the function you’re calling references multiple tables and the data should be retrieve in a single atomic operation. In our case, we’re getting data from both the games and players tables.

@Transaction
@Query("SELECT * FROM games ORDER BY startTime DESC LIMIT 1")
abstract fun getCurrentGameWithPlayers(): LiveData<GameWithPlayers>

While the query only mentions the games table, we’re actually pulling in data from both tables due to the @Relation annotation and the @Junction on the GameStatus class. That tells Room to get the associated Player records for the Game without having to write out that piece of the SQL query.

The next function is getCurrentGameStatuses(). Unfortunately, there isn’t an easy way with Room to grab a game, the players, and the statuses for each player in a single query and map the results to an object. Therefore, we need to pull in the GameStatus objects separately. This @Query will get the latest GameStatus instance by performing a subquery on the games table. We get the most recent open game then sort the statuses by the gamePlayerNumber property (to ensure players are in right play order). Note that this will also be a @Transaction since we’re referencing multiple tables.

@Transaction
@Query(
    """
    SELECT * FROM game_statuses
    WHERE gameId = (
    SELECT gameId FROM games
    WHERE endTime IS NULL
    ORDER BY startTime DESC
    LIMIT 1)
    ORDER BY gamePlayerNumber
    """
)
abstract fun getCurrentGameStatuses(): LiveData<List<GameStatus>>

As you can see, the @Query annotation gives us a lot of flexibility in retrieving data from the database. However, sometimes a @Query still isn’t enough and we need to call multiple functions in a single @Transaction. For that scenario, we can instead create an open function and implement the function ourselves rather than letting Room do that for us. In fact, the ability to have fully implemented functions in our DAO is the reason PennyDropDao is an abstract class rather than an interface (as is commonly seen in the Room documentation).

In the case of startGame(), we’re going to bring in a List<Player>, close up any existing games, create a new Game, get or insert the entered Player objects from/into the database, then add new GameStatus entries for each player before returning the newly created game’s ID. To do all this, we’re going to be calling other functions inside PennyDropDao to do the work for us. We already created insertGame() and insertPlayer() when first building PennyDropDao, so we just need two additional new functions.

The first function is called closeOpenGames(), which goes through the database and sets the current time as the endTime and state of “Cancelled” for any still open games. We previously saw named bind parameters earlier in Chapter 5 but here they’re a bit more interesting.

We can’t send in complex types by default, but since we previously created type converters for both types, this works just fine:

@Query("""
	UPDATE games
	SET endTime = :endDate, gameState = :gameState
	WHERE endTime IS NULL""")
abstract suspend fun closeOpenGames(
	endDate: OffsetDateTime = OffsetDateTime.now(),
	gameState: GameState = GameState.Cancelled
)

Note the use of default values for each property. We can include parameters we have no intention of overwriting purely to be able to include them as parameters in a query, yet still keep the flexibility to overwrite if needed for any reason.

Also, since this function is modifying the database, it needs to be a transaction. But instead of having to add the @Transaction annotation, Room automatically wraps all modifying actions as a transaction. This includes functions with an @Insert, @Update, or @Delete annotation.

The other function we still need is insertGameStatuses(), which just requires the @Insert annotation.

@Insert
abstract suspend fun insertGameStatuses(gameStatuses: List<GameStatus>)

This highlights another nice Room feature: we can send in a List<GameStatus> and all GameStatus records are entered into the database instead of manually having to insert them one by one.

Now that all the functions we’re using are created, we can get back to startGame() itself. Note that even though we have the implementation for startGame() in here, it still has to be marked as open since it has the @Transaction annotation. The function code looks like this:

@Transaction
open suspend fun startGame(players: List<Player>): Long {
    this.closeOpenGames()

    val gameId = this.insertGame(
        Game(
            gameState = GameState.Started,
            currentTurnText = "The game has begun!\n",
            canRoll = true
        )
    )

    val playerIds = players.map { player ->
        getPlayer(player.playerName)?.playerId ?: insertPlayer(player)
    }

    this.insertGameStatuses(
        playerIds.mapIndexed { index, playerId ->
            GameStatus(
                gameId,
                playerId,
                index,
                index == 0
            )
        }
    )

    return gameId
}

The one piece I want to call out here is how we’re getting the playerIds value. We check the database for a player and either use that to get the playerId or, if the player’s not found, we create the player and then send back its player ID. Since insertPlayer() returns a Long, we get back the database ID right away without having to do a secondary lookup. Plus, the Elvis operator (?:) allows us to keep everything in one expression instead of having to include extra conditional logic.

We have one remaining function to cover in the PennyDropDao, which is the updateGameAndStatuses() function. This function does exactly what you’d expect: it updates the DB versions of the entered Game and GameStatus objects, all wrapped in a single @Transaction. updateGame() already exists, but we need to create updateGameStatuses() quickly:

@Update
abstract suspend fun updateGameStatuses(gameStatuses: List<GameStatus>)

From there, updateGameAndStatuses() is just calling those two functions, wrapped in a @Transaction:

@Transaction
open suspend fun updateGameAndStatuses(
    game: Game,
    statuses: List<GameStatus>
) {
    this.updateGame(game)
    this.updateGameStatuses(statuses)
}

The last part of the PennyDropRepository I want to cover is adding the ability to have a singleton instance of the repository for use anywhere. This is optional, but will be useful to avoid creating multiple instances of PennyDropRepository for different views.

The idea is the same as with PennyDropDatabase: we get the existing instance variable unless it’s null, then we create a new instance and return that. We’re also going to take advantage of the synchronized() block to avoid having multiple simultaneous attempts at creating the PennyDropRepository. All of this will live inside PennyDropRepository in its companion object:

companion object {
    @Volatile
    private var instance: PennyDropRepository? = null

    fun getInstance(pennyDropDao: PennyDropDao) =
        this.instance ?: synchronized(this) {
            instance ?: PennyDropRepository(pennyDropDao).also {
                instance = it
            }
        }
}

With that, the PennyDropRepository and PennyDropDao classes are now all set. We have all the logic we need to persist our game data in a local database. The remaining piece is to pull that data back out of the database and use it inside GameViewModel.

Thanks for Reading

Hopefully you enjoyed this preview of “Kotlin and Android Development featuring Jetpack” and gained some insight into an Android Repository class. Repositories are a great way to set up a single access point to your database without making the rest of your app care about your data storage strategy.

Want to see more about the Penny Drop app or Android development? “Kotlin and Android Development featuring Jetpack” can be found in eBook form (PDF/ePub/mobi) on PragProg.com and soon as a physical book from various fine booksellers.

For more info about a Repository class and Android app architecture, check out the Android Developer’s “Guide to App Architecture”.