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:
- The data persistence library, Room.
- Using the Retrofit networking library.
- The basic Android Architecture Components,
ViewModel
,ViewModelFactory
, andLiveData
. - Transformations for a LiveData class.
- Building and launching a coroutine.
- Binding adapters in data binding.
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
- Click on the provided URL. This opens the GitHub page for the project in a browser.
- 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.
- On the GitHub page for the project, click the Code button, which brings up a popup.
- In the popup, click the Download ZIP button to save the project to your computer. Wait for the download to complete.
- Locate the file on your computer (likely in the Downloads folder).
- Double-click the ZIP file to unpack it. This creates a new folder that contains the project files.
Open the project in Android Studio
- Start Android Studio.
- In the Welcome to Android Studio window, click Open.
Note: If Android Studio is already open, instead, select the File > Open menu option.
- In the file browser, navigate to where the unzipped project folder is located (likely in your Downloads folder).
- Double-click on that project folder.
- Wait for Android Studio to open the project.
- Click the Run button
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.
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.
- Temporarily turn on airplane mode in the Android emulator (Settings App > Network & Internet > Airplane mode).
- Run the DevBytes app and observe that the screen is blank.
- 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.
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.
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.
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 | 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
- In repository/VideosRepository.kt, create a
VideosRepository
class. Pass in aVideosDatabase
object as the class's constructor parameter to access the DAO methods.
class VideosRepository(private val database: VideosDatabase) {
}
- Inside the
VideosRepository
class, add asuspend
method calledrefreshVideos()
that has no arguments and returns nothing. This method will be the API used to refresh the offline cache.
suspend fun refreshVideos() {
}
- Inside the
refreshVideos()
method, switch the coroutine context toDispatchers.IO
to perform network and database operations.
suspend fun refreshVideos() {
withContext(Dispatchers.IO) {
}
}
- Inside the
withContext
block, fetch theDevByte
video playlist from the network using the Retrofit service instance,DevByteNetwork
.
val playlist = DevByteNetwork.devbytes.getPlaylist()
- Inside the
refreshVideos()
method, after fetching the playlist from the network, store the playlist in the Room database. To store the playlist, use theVideosDatabase
class. Call theinsertAll()
DAO method, passing in the playlist retrieved from the network. Use theasDatabaseModel()
extension function to map the playlist to the database object.
database.videoDao.insertAll(playlist.asDatabaseModel())
- 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.
- In the
VideosRepository
class, declare aLiveData
object calledvideos
to hold a list ofDevByteVideo
objects. Initialize thevideos
object usingdatabase.videoDao
. Call thegetVideos()
DAO method. Because thegetVideos()
method returns a list of database objects, and not a list ofDevByteVideo
objects, Android Studio throws a "type mismatch" error.
val videos: LiveData<List<DevByteVideo>> = database.videoDao.getVideos()
- To fix the error, use
Transformations.map
to convert the list of database objects to a list of domain objects using theasDomainModel()
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.
- In viewmodels/DevByteViewModel.kt, inside
DevByteViewModel
class, create a private member variable calledvideosRepository
of the typeVideosRepository
. Instantiate the variable by passing in the singletonVideosDatabase
object.
private val videosRepository = VideosRepository(getDatabase(application))
- In the
DevByteViewModel
class, replace therefreshDataFromNetwork()
method with therefreshDataFromRepository()
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
}
}
}
- In the
DevByteViewModel
class, inside theinit
block, change the function call fromrefreshDataFromNetwork()
torefreshDataFromRepository()
. This code fetches the video playlist from the repository, not directly from the network.
init {
refreshDataFromRepository()
}
- 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
- In the
DevByteViewModel
class, after instantiating thevideosRepository
object, add a newval
calledplaylist
for holding aLiveData
list of videos from the repository.
val playlist = videosRepository.videos
- 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.
- To notice the difference, enable airplane mode on the emulator or device.
- 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.
- Turn off airplane mode in the emulator or device.
- 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.