Accessing the Database Using the Repository Pattern

To access your database, you will use the repository pattern recommended by Google in its Guide to App Architecture (developer.android.com/​jetpack/​docs/​guide).

A repository class encapsulates the logic for accessing data from a single source or a set of sources. It determines how to fetch and store a particular set of data, whether locally in a database or from a remote server. Your UI code will request all the data from the repository, because the UI does not care how the data is actually stored or fetched. Those are implementation details of the repository itself.

Since CriminalIntent is a simpler app, the repository will only handle fetching data from the database.

Create a class called CrimeRepository in the com.bignerdranch.android.criminalintent package and define a companion object in the class.

Listing 11.9  Implementing a repository (CrimeRepository.kt)

class CrimeRepository private constructor(context: Context) {

     companion object {
        private var INSTANCE: CrimeRepository? = null

         fun initialize(context: Context) {
             if (INSTANCE == null) {
                 INSTANCE = CrimeRepository(context)
             }
        }

         fun get(): CrimeRepository {
            return INSTANCE ?:
            throw IllegalStateException("CrimeRepository must be initialized")
        }
    }
}
      

CrimeRepository is a singleton. This means there will only ever be one instance of it in your app process.

A singleton exists as long as the application stays in memory, so storing any properties on the singleton will keep them available throughout any lifecycle changes in your activities and fragments. Be careful with singleton classes, as they are destroyed when Android removes your application from memory. The CrimeRepository singleton is not a solution for long-term storage of data. Instead, it gives the app an owner for the crime data and provides a way to easily pass that data between controller classes.

To make CrimeRepository a singleton, you add two functions to its companion object. One initializes a new instance of the repository, and the other accesses the repository. You also mark the constructor as private to ensure no components can go rogue and create their own instance.

The getter function is not very nice if you have not called initialize() before it. It will throw an IllegalStateException, so you need to make sure that you initialize your repository when your application is starting.

To do work as soon as your application is ready, you can create an Application subclass. This allows you to access lifecycle information about the application itself. Create a class called CriminalIntentApplication that extends Application and override Application.onCreate() to set up the repository initialization.

Listing 11.10  Creating an application subclass (CriminalIntentApplication.kt)

class CriminalIntentApplication : Application() {

    override fun onCreate() {
        super.onCreate()
        CrimeRepository.initialize(this)
    }
}
      

Similar to Activity.onCreate(…), Application.onCreate() is called by the system when your application is first loaded in to memory. That makes it a good place to do any kind of one-time initialization operations.

The application instance does not get constantly destroyed and re-created, like your activity or fragment classes. It is created when the app launches and destroyed when your app process is destroyed. The only lifecycle function you will override in CriminalIntent is onCreate().

In a moment, you are going to pass the application instance to your repository as a Context object. This object is valid as long as your application process is in memory, so it is safe to hold a reference to it in the repository class.

But in order for your application class to be used by the system, you need to register it in your manifest. Open AndroidManifest.xml and specify the android:name property to set up your application.

Listing 11.11  Hooking up the application subclass (manifests/AndroidManifest.xml)

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.bignerdranch.android.criminalintent">

    <application
            android:name=".CriminalIntentApplication"
            android:allowBackup="true"
            ... >
        ...
    </application>

</manifest>

With the application class registered in the manifest, the OS will create an instance of CriminalIntentApplication when launching your app. The OS will then call onCreate() on the CriminalIntentApplication instance. Your CrimeRepository will be initialized, and you can access it from your other components.

Next, add two properties on your CrimeRepository to store references to your database and DAO objects.

Listing 11.12  Setting up repository properties (CrimeRepository.kt)

private const val DATABASE_NAME = "crime-database"

class CrimeRepository private constructor(context: Context) {

    private val database : CrimeDatabase = Room.databaseBuilder(
        context.applicationContext,
        CrimeDatabase::class.java,
        DATABASE_NAME
    ).build()

    private val crimeDao = database.crimeDao()

    companion object {
        ...
    }
}

Room.databaseBuilder() creates a concrete implementation of your abstract CrimeDatabase using three parameters. It first needs a Context object, since the database is accessing the filesystem. You pass in the application context because, as discussed above, the singleton will most likely live longer than any of your activity classes.

The second parameter is the database class that you want Room to create. The third is the name of the database file you want Room to create for you. You are using a private string constant defined in the same file, since no other components need to access it.

Next, fill out your CrimeRepository so your other components can perform any operations they need to on your database. Add a function to your repository for each function in your DAO.

Listing 11.13  Adding repository functions (CrimeRepository.kt)

class CrimeRepository private constructor(context: Context) {

    ...
    private val crimeDao = database.crimeDao()

    fun getCrimes(): List<Crime> = crimeDao.getCrimes()

    fun getCrime(id: UUID): Crime? = crimeDao.getCrime(id)

    companion object {
        ...
    }
}

Since Room provides the query implementations in the DAO, you call through to those implementations from your repository. This helps keep your repository code short and easy to understand.

This may seem like a lot of work for little gain, since the repository is just calling through to functions on your CrimeDao. But fear not; you will be adding functionality soon to encapsulate additional work the repository needs to handle.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.91.8.23