Repository Pattern

1. Before you begin

Introduction

In this codelab, you'll improve the user experience for an app by using offline caching. Many apps rely on data from the network. If your app fetches data from the server on every launch, and the user sees a loading screen, it might be a bad user experience leading to users uninstalling your app.

When users launch an app, they expect the app to show data quickly. You can achieve this goal by implementing offline caching. Offline caching means that your app saves data fetched from the network on the device's local storage, resulting in faster access.

Because the app will be able to get data from the network as well as keep an offline cache of previously downloaded results, you'll need a way for your app to organize these multiple sources of data. You'll do this by implementing a repository class, which will serve as a single source of truth for the app's data, and abstract the source of the data (network, cache, etc.) out of the view model.

What you should already know

You should be familiar with:

What you'll learn

  • How to implement a repository to abstract an app's data layer from the rest of the app.
  • How to load cached data using a repository.

What you'll do

  • Use a repository to abstract the data layer, and integrate the repository class with the ViewModel.
  • Display data from the offline cache.

2. Starter Code

Download the project code

Note that the folder name is RepositoryPattern-Starter. Select this folder when you open the project in Android Studio.

To get the code for this codelab and open it in Android Studio, do the following.

Get the code

  1. Click on the provided URL. This opens the GitHub page for the project in a browser.
  2. Check and confirm the branch name matches with the branch name specified in the codelab. For example, in the following screenshot the branch name is main.

8cf29fa81a862adb.png

  1. On the GitHub page for the project, click the Code button, which brings up a popup.

1debcf330fd04c7b.png

  1. In the popup, click the Download ZIP button to save the project to your computer. Wait for the download to complete.
  2. Locate the file on your computer (likely in the Downloads folder).
  3. Double-click the ZIP file to unpack it. This creates a new folder that contains the project files.

Open the project in Android Studio

  1. Start Android Studio.
  2. In the Welcome to Android Studio window, click Open.

d8e9dbdeafe9038a.png

Note: If Android Studio is already open, instead, select the File > Open menu option.

8d1fda7396afe8e5.png

  1. In the file browser, navigate to where the unzipped project folder is located (likely in your Downloads folder).
  2. Double-click on that project folder.
  3. Wait for Android Studio to open the project.
  4. Click the Run button 8de56cba7583251f.png to build and run the app. Make sure it builds as expected.

3. Starter app overview

The DevBytes app presents a list of DevBytes videos from the Android Developers Youtube Channel in a recycler view, where users can click to open a link to the video.

9757e53b89d2de7c.png

Although the starter code is fully working, it has a major flaw that can negatively affect the user experience. If the user has a spotty connection, or no Internet connection at all, none of the videos will be displayed. This happens even if the app was opened previously. If the user exits and relaunches the app again, this time without Internet, the app attempts and fails to redownload the list of videos.

You can see this in action on the emulator.

  1. Temporarily turn on airplane mode in the Android emulator (Settings App > Network & Internet > Airplane mode).
  2. Run the DevBytes app and observe that the screen is blank.

f0365b27d0dd8f78.png

  1. Be sure to turn Airplane mode off before proceeding with the rest of the codelab.

This is because after the DevBytes app downloads the data for the first time, nothing is cached for later use. The app currently includes a Room database. Your task is to use it to implement caching functionality and update the view model to use a repository, which will either download new data, or fetch it from the Room database. The repository class abstracts this logic away from the view model, keeping your code organized and decoupled.

The starter project is organized into several packages.

25b5f8d0997df54c.png

While you're welcome and encouraged to familiarize yourself with the code, you'll only be touching two files: repository/VideosRepository.kt, and viewmodels/DevByteViewModel. First, you'll create a VideosRepository class that implements the repository pattern for caching (you'll learn more about this in the next few pages), and then update the DevByteViewModel to use your new VideosRepository class.

Before you jump right into the code, however, let's take a moment to learn more about caching and the repository pattern.

4. Caching and the repository pattern

Repositories

The repository pattern is a design pattern that isolates the data layer from the rest of the app. The data layer refers to the part of your app, separate from the UI, that handles the app's data and business logic, exposing consistent APIs for the rest of your app to access this data. While the UI presents information to the user, the data layer includes things like networking code, Room databases, error handling, and any code that reads or manipulates data.

9e528301efd49aea.png

A repository can resolve conflicts between data sources (such as persistent models, web services, and caches) and centralize changes to this data. The diagram below shows how app components such as activities might interact with data sources by way of a repository.

69021c8142d29198.png

To implement a repository, you use a separate class, such as the VideosRepository class that you create in the next task. The repository class isolates the data sources from the rest of the app and provides a clean API for data access to the rest of the app. Using a repository class ensures this code is separate from the ViewModel class, and is a recommended best practice for code separation and architecture.

Advantages of using a repository

A repository module handles data operations and lets you use multiple backends. In a typical real-world app, the repository implements the logic for deciding whether to fetch data from a network or use results that are cached in a local database. With a repository, you can swap out the implementation details, such as migrating to a different persistence library, without affecting the calling code, such as the view models. This also helps make your code modular and testable. You can easily mock up the repository and test the rest of the code.

A repository should serve as a single source of truth for a particular piece of your app's data. When working with multiple data sources, such as both a networked resource and an offline cache, the repository ensures the app's data is as accurate and up-to-date as possible, providing the best possible experience even when the app is offline.

Caching

A cache refers to a storage of data used by your app. For example, you may want to temporarily save data from the network in case the user's Internet connection is interrupted. Even though the network is no longer available, the app can still fall back on the cached data. A cache might also be useful for storing temporary data for an activity that's no longer on screen, or even persisting data in-between app launches.

A cache can take many forms, some simpler or more complex, depending on the specific task. The following table shows several ways to implement network caching in Android.

Caching technique

Uses

Retrofit is a networking library used to implement a type-safe REST client for Android. You can configure Retrofit to store a copy of every network result locally.

Good solution for simple requests and responses, infrequent network calls, or small datasets.

You can use DataStore to store key-value pairs.

Good solution for a small number of keys and simple values, such as app settings. You can't use this technique to store large amounts of structured data.

You can access the app's internal storage directory and save data files in it. Your app's package name specifies the app's internal storage directory, which is in a special location in the Android file system. This directory is private to your app, and it is cleared when your app is uninstalled.

Good solution if you have specific needs that a file system can solve - for example, if you need to save media files or data files and you have to manage the files yourself. You can't use this technique to store complex and structured data that your app needs to query.

You can cache data using Room, which is an SQLite object-mapping library that provides an abstraction layer over SQLite.

Recommended solution for complex structured queryable data, because the best way to store structured data on a device's file system is in a local SQLite database.

In this codelab, you use Room, because it's the recommended way to store structured data on a device file system. The DevBytes app is already configured to use Room. Your task is to implement offline caching using the repository pattern to separate the data layer from the UI code.

5. Implement VideosRepository

Task: Create a repository

In this task, you'll create a repository to manage the offline cache, which you implemented in the previous task. Your Room database doesn't have logic for managing the offline cache, it only has methods to insert, update, delete, and retrieve the data. The repository will have the logic to fetch the network results and to keep the database up-to-date.

Step 1: Add a repository

  1. In repository/VideosRepository.kt, create a VideosRepository class. Pass in a VideosDatabase object as the class's constructor parameter to access the DAO methods.
class VideosRepository(private val database: VideosDatabase) {
}
  1. Inside the VideosRepository class, add a suspend method called refreshVideos() that has no arguments and returns nothing. This method will be the API used to refresh the offline cache.
suspend fun refreshVideos() {
}
  1. Inside the refreshVideos() method, switch the coroutine context to Dispatchers.IO to perform network and database operations.
suspend fun refreshVideos() {
   withContext(Dispatchers.IO) {
   }
}
  1. Inside the withContext block, fetch the DevByte video playlist from the network using the Retrofit service instance, DevByteNetwork.
val playlist = DevByteNetwork.devbytes.getPlaylist()      
  1. Inside the refreshVideos() method, after fetching the playlist from the network, store the playlist in the Room database. To store the playlist, use the VideosDatabase class. Call the insertAll() DAO method, passing in the playlist retrieved from the network. Use the asDatabaseModel() extension function to map the playlist to the database object.
database.videoDao.insertAll(playlist.asDatabaseModel())
  1. Here is the complete refreshVideos() method with a log statement for tracking when it gets called:
suspend fun refreshVideos() {
   withContext(Dispatchers.IO) {
       val playlist = DevByteNetwork.devbytes.getPlaylist()
       database.videoDao.insertAll(playlist.asDatabaseModel())
   }
}

Step 2: Retrieve data from the database

In this step, you create a LiveData object to read the video playlist from the database. This LiveData object is automatically updated when the database is updated. The attached fragment, or the activity, is refreshed with new values.

  1. In the VideosRepository class, declare a LiveData object called videos to hold a list of DevByteVideo objects. Initialize the videos object using database.videoDao. Call the getVideos() DAO method. Because the getVideos() method returns a list of database objects, and not a list of DevByteVideo objects, Android Studio throws a "type mismatch" error.
val videos: LiveData<List<DevByteVideo>> = database.videoDao.getVideos()
  1. To fix the error, use Transformations.map to convert the list of database objects to a list of domain objects using the asDomainModel() conversion function.
val videos: LiveData<List<DevByteVideo>> = Transformations.map(database.videoDao.getVideos()) {
   it.asDomainModel()
}

Now you've implemented a repository for your app. In the next task, you use a simple refresh strategy to keep the local database up-to-date.

6. Use VideosRepository in DevByteViewModel

Task: Integrate the repository using a refresh strategy

In this task, you integrate your repository with the ViewModel using a simple refresh strategy. You display the video playlist from the Room database, not directly fetching from the network.

A database refresh is a process of updating or refreshing the local database to keep it in sync with data from the network. For this sample app, you'll use a simple refresh strategy, where the module that requests data from the repository is responsible for refreshing the local data.

In a real-world app, your strategy might be more complex. For example, your code might automatically refresh the data in the background (taking bandwidth into account), or cache the data that the user is most likely to use next.

  1. In viewmodels/DevByteViewModel.kt, inside DevByteViewModel class, create a private member variable called videosRepository of the type VideosRepository. Instantiate the variable by passing in the singleton VideosDatabase object.
private val videosRepository = VideosRepository(getDatabase(application))
  1. In the DevByteViewModel class, replace the refreshDataFromNetwork() method with the refreshDataFromRepository() method. The old method, refreshDataFromNetwork(), fetched the video playlist from the network using the Retrofit library. The new method loads the video playlist from the repository. The repository determines which source (e.g., the network, database, etc.) the playlist is retrieved from, keeping the implementation details out of the view model. The repository also makes your code more maintainable; if you were to change the implementation for getting the data in the future, you would not need to modify the view model.
private fun refreshDataFromRepository() {
   viewModelScope.launch {
       try {
           videosRepository.refreshVideos()
           _eventNetworkError.value = false
           _isNetworkErrorShown.value = false

       } catch (networkError: IOException) {
           // Show a Toast error message and hide the progress bar.
           if(playlist.value.isNullOrEmpty())
               _eventNetworkError.value = true
       }
   }
}
  1. In the DevByteViewModel class, inside the init block, change the function call from refreshDataFromNetwork() to refreshDataFromRepository(). This code fetches the video playlist from the repository, not directly from the network.
init {
   refreshDataFromRepository()
}
  1. In the DevByteViewModel class, delete the _playlist property and its backing property, playlist.

Code to delete

private val _playlist = MutableLiveData<List<Video>>()
...
val playlist: LiveData<List<Video>>
   get() = _playlist
  1. In the DevByteViewModel class, after instantiating the videosRepository object, add a new val called playlist for holding a LiveData list of videos from the repository.
val playlist = videosRepository.videos
  1. Run your app. The app runs as before, but now the DevBytes playlist is fetched from the network and saved in the Room database. The playlist is displayed on the screen from the Room database, not directly from the network.

30ee74d946a2f6ca.png

  1. To notice the difference, enable airplane mode on the emulator or device.
  2. Run the app once again. Notice that the "Network Error" toast message is not displayed. Instead the playlist is fetched from the offline cache and displayed.
  3. Turn off airplane mode in the emulator or device.
  4. Close and re-open the app. The app loads the playlist from the offline cache, while the network request runs in the background.

If new data came in from the network, the screen would automatically update to show the new data. However, the DevBytes server does not refresh its contents, so you do not see the data updating.

Great work! In this codelab, you integrated an offline cache with a ViewModel to display the playlist from the repository instead of fetching the playlist from the network.

7. Solution Code

Solution code

Android Studio project: RepositoryPattern

8. Congratulations

Congratulations! In this pathway you learned:

  • Caching is a process of storing data fetched from a network on a device's storage. Caching lets your app access data when the device is offline, or if your app needs to access the same data again.
  • The best way for your app to store structured data on a device's file system is to use a local SQLite database. Room is an SQLite object-mapping library, meaning that it provides an abstraction layer over SQLite. Using Room is the recommended best practice for implementing offline caching.
  • A repository class isolates data sources, such as a Room database and web services, from the rest of the app. The repository class provides a clean API for data access to the rest of the app.
  • Using repositories is a recommended best practice for code separation and architecture.
  • When you design an offline cache, it's a best practice to separate the app's network, domain, and database objects. This strategy is an example of separation of concerns.

Learn more