Why learn about Architecture Components libraries?

Architecture Components are a set of Android libraries for structuring your app in a way that is robust, testable, and maintainable. In addition to the libraries, there is also the Guide to App Architecture which introduces an approach to architecting an Android app using the Architecture Component libraries.

By learning to work with Architecture Components, you'll write apps with less boilerplate code and you'll see strategies for dealing with tricky issues involving lifecycles and persistence.

What you'll build

In this codelab you'll use the different components to make a weather app called Sunshine that pulls data from a remote source, stores it locally, and displays it to the user.

You will build your app to:

What you'll need

Get the Code

In this step, you download the code for the entire codelab.

Download the Code

  1. Download the starting code from the link above.
  2. Unzip the code.
  3. Import into Android Studio 3.0. This may take several minutes.

If you want to see all the steps taken to build this app, you can look at the github history.

Sunshine: then and now

Sunshine is a weather app that is used in Google's Developing Android Apps course on Udacity. In this codelab, you'll take the skeleton code for this app and add the Architecture Component libraries and architecture patterns found in the Guide to app Architecture.

For your app the following should be true:

Our resulting app will have two screens, a screen that lists a fourteen-day weather forecast, and a detail screen that gives additional details about a single day's weather forecast:

MainActivity

DetailActivity

Opinionated Guide to App Architecture

You'll be following the Guide to App Architecture's recommended app architecture. Below is a diagram of the different classes involved. Don't worry if you're not familiar with all of the components - you'll be learning about them in this codelab.

Here's a summary explanation of the different classes in the diagram, starting from the top:

UI Controllers - UI Controllers are activities or fragments. The only job of UI controllers is to know how to display data and pass on UI events, such as the user pressing a button. UI Controllers neither contain the UI data, nor directly manipulate data.

ViewModels and LiveData - These classes represent all of the data needed for the UI to display. You'll learn exactly how these two classes function together in this codelab.

Repository - This class is the single source of truth for all of our app's data and acts as a clean API for the UI to communicate with. ViewModels simply request data from the repository. They do not need to worry about whether the repository should load from the database or network, or how or when to persist the data. The repository manages all of this. As part of this responsibility, the repository is a mediator between the different data sources. You'll learn a lot more about repositories when you make one in this codelab.

Remote Network Data Source - Manages data from a remote data source, such as the internet.

Model - Manages local data stored in the database.

The Sunshine Starting Code

The starting code contains two activities, the MainActivity and the DetailActivity.

MainActivity

DetailActivity

The DetailActivity is the first activity you will work on. All of the UI code needed to properly display list items and all the image resources are in the starter code, the app is just not yet connected to the data sources.

You can find out more about the classes in the app by looking at the README.

Sunshine: Architecture

Let's quickly look at what the final architecture of the app will be.

There will be two activities (MainActivity and DetailActivity) with their own ViewModels (MainActivityViewModel and DetailActivityViewModel) and associated LiveData. They will use a repository class (SunshineRepository), which will manage communications between a SQLite database and a network data source. The WeatherNetworkDataSource requests weather data from a mock weather server using two services (SunshineSyncIntentService and SunshineFirebaseJobService). The mock weather server returns randomized JSON data.

The classes outlined in green are what you'll be building from scratch. Let's get started with the database portion of the app and learning about Room, which is a SQLite object mapping library for Android.

Why Cache Weather Data

Most, if not all, apps do something with data. In Sunshine's case, you have WeatherEntry data objects, representing weather forecasts. You could decide that every time you create an activity in Sunshine, you download the latest weather data from the server. This strategy ensures that the user will see the most up-to-date weather, but is incredibly inefficient. Every time you switch screens or rotate the phone, you'll be re-downloading weather data, and most of the time it won't have changed! In addition, if the user is offline, there's no way they can use your app.

This is why most apps will save some data on the phone in a local data cache. Android provides full support for local SQLite databases, so SQLite is a common database format chosen for this cache.

Introduction to Room

Benefits of Room

Working with SQLite databases on Android means using APIs like SQLiteOpenHelper, SQLiteDatabase and SQLiteQueryBuilder. Although powerful, they also present a number of development challenges, including lots of boilerplate code, and no easy way to validate your SQLite statements during compile time.

For Sunshine, you're going to use the new SQLite object mapping library, Room. Room comes with a number of benefits over the built-in APIs, including:

Components of Room

Room uses annotations to define database structure. It has three major components:

Your Turn: Add Room to Sunshine

Add the Room library to your project:

  1. Open the build.gradle file for your project (not the ones for your app or module). You should see the following already there:
allprojects {
    repositories {
        jcenter()
        maven { url 'https://maven.google.com' }
    }
}
  1. Open the build.gradle file for your app or module and add the following dependencies:
compile "android.arch.persistence.room:runtime:1.0.0-alpha9"
annotationProcessor "android.arch.persistence.room:compiler:1.0.0-alpha9"
  1. Sync gradle.

These dependencies provide Room and its robust annotation processor, which you'll use now to create an Entity.

The code diff for this step can be viewed here.

How to Create Entities

To understand how entities are created, let's look at an example from the Room documentation. Then you'll apply what you've learned to Sunshine.

Room uses annotations to define table structure and column constraints for the table Room generates for you. Let's say you wanted to make a table that looks like this for user objects:

users table

id (Primary Key for table)

firstName

lastName

1

Florina

Muntenescu

2

Lyla

Fujiwara

3

Yigit

Boyar

This is the code that would create that table:

// Creates a table named users. 
// tableName is the property name, users is the value
@Entity(tableName = "users") 
class User {
    @PrimaryKey // Denotes id as the primary key
    public int id;
    public String firstName;
    public String lastName;

    @Ignore // Tells Room to ignore this field
    Bitmap picture;
}

There are a few things entities must have, which you can see in the example:

By default, this will create a table named after the class, with columns named after the field names. Room has other annotations and properties for those annotations. In the example, you can see the @Entity annotation has the property tableName, which means that instead of having "user" (the class name) be this table's name, it's named "users"

Your Turn: Create an Entity for a single weather forecast

Our database will have a single table which stores weather values:

weather table

id (Primary Key)

weatherIconId

date

min

max

humidity

pressure

wind

degrees

1

500

1502668800000

13.32

18.27

96

996.68

1.2

0

2

501

1502755200000

12.66

17.34

97

996.12

4.8

45

3

800

1502841600000

12.07

16.48

90

995.7

8.2

90

When starting from scratch, you'd need to create an object to model the values to save in this table. The starter app already has a model class WeatherEntry. Go look at it now.

Convert your data.database.WeatherEntry model to a Room Entity by doing the following:

  1. Make WeatherEntry a @Entity and change the table name to "weather": Right above the WeatherEntry class, add the @Entity annotation. Add the tableName property and set the value to "weather". Without this, the table name would have been "weatherentry".
@Entity(tableName = "weather")
  1. Define the id as an autogenerated primary key: Above the id field, place the @PrimaryKey annotation. The Sunshine code doesn't have a unique database id for each WeatherEntry because the weather server doesn't return one. To have Room do this for you, add the autoGenerate property to the @PrimaryKey annotation and set its value to true.
@PrimaryKey(autoGenerate = true)
  1. The date field should be unique: The date field is unique because we're only storing the weather for one location, so there should never be two different weather forecasts for a given day. Add the indices property to the @Entity annotation (which is above the class), in addition to the tableName property. The value should be the date column and unique should be set to true.
@Entity(tableName = "weather", indices = {@Index(value = {"date"}, unique = true)})
  1. Provide Room access to the fields: In your case, you want the WeatherEntry class to be read-only: Sunshine will download and display weather data, but it won't ever modify this weather data.

    To achieve this, keep the fields private, keep the provided getter methods and create an additional constructor that allows Room to set every single field of WeatherEntry. This makes it possible for Room to make WeatherEntity objects for us, but protects those objects from editing after they've been constructed.
    public WeatherEntry(int id, int weatherIconId, Date date, double min, double max, double humidity, double pressure, double wind, double degrees) {
        this.id = id;
        this.weatherIconId = weatherIconId;
        this.date = date;
        this.min = min;
        this.max = max;
        this.humidity = humidity;
        this.pressure = pressure;
        this.wind = wind;
        this.degrees = degrees;
    }
  1. Only one constructor should be exposed to Room: Room cannot compile an entity with two constructors because it doesn't know which one to use. Since the constructor without int id is not needed by Room, you can hide it from Room using the @Ignore annotation.

The code diff for this step can be viewed here.

How to Create a DAO(Database Access Object)

Next, you'll create a @Dao, short for database access object, for your WeatherEntry entity. DAOs are either abstract classes or interfaces that define the read and write actions you can apply to your database data. Let's look at a simple example DAO from the documentation, for the example User.java entity from before:

@Dao // Required annotation for Dao to be recognized by Room
public interface UserDao {
    // Returns a list of all users in the database
    @Query("SELECT * FROM user")
    List<User> getAll();

    // Inserts multiple users
    @Insert
    void insertAll(User... users);

    // Deletes a single user
    @Delete
    void delete(User user);
}

The only thing a DAO needs is the @Dao annotation. To make your DAO useful, you define method signatures and annotate them with @Insert, @Delete, @Update and @Query. @Insert, @Delete and @Update are convenience annotations that create methods which do as their name implies.

The example void insertAll(User... users); shows how you can specify that you can insert a variable number of Users -- this method will accept any number of User objects or an array of User objects and insert them into the database.

As you can see from the example, you can pass in the User entity object as a parameter or return a User entity object from Dao methods. You can do this with any entity class.

@Query with Parameters

If what you want to do is not covered by the convenience annotations, use @Query. @Query lets you write SQLite to create read/write database operations. Notably, you can include parameters passed into the method annotated with @Query, by appending a colon (:) to the parameter in the query string. For example, if you were defining a method that finds a user by name, it might look like:

@Query("SELECT * FROM user WHERE first_name LIKE :first AND "
           + "last_name LIKE :last LIMIT 1")
User findByName(String first, String last);

The first and last parameters are included in the query string as :first and :last. So if you call the method findByName("Jane", "Doe") the query SELECT * FROM user WHERE first_name LIKE Jane AND last_name LIKE Doe LIMIT 1 will be called and return a row that is automatically converted to a User object.

Your Turn: Create a DAO for WeatherEntry

Create a DAO for WeatherEntry called WeatherDao. The steps are:

  1. In the data.database package (the same package as WeatherEntry), make a new interface called WeatherDao.java.
  2. Annotate the interface WeatherDao with @Dao.
  3. Define the void bulkInsert method, which inserts any number of WeatherEntry objects. As the app receives lists of weather entries from the server, it'll use bulkInsert to put them into the database.
@Insert
void bulkInsert(WeatherEntry... weather);
  1. Additionally, for bulkInsert, you want to use a OnConflictStrategy.REPLACE so that when Sunshine re-downloads forecasts, old weather forecasts are replaced by new ones. You can use an annotation property for this, for example:
@Insert(onConflict = OnConflictStrategy.REPLACE)
  1. Define the getWeatherByDate method which takes a Java.util.Date object and returns the weather for that date. The Date object doesn't convert to a String value for the purpose of a query. You'll be learning about TypeConverters in the next step for Date objects. For now, know there is a way for Room to automatically convert Date objects to longs, and assume you can use your Date parameter as if it were a long.
@Query("SELECT * FROM weather WHERE date = :date")
WeatherEntry getWeatherByDate(Date date);

The code diff for this step can be viewed here.

How to Create a Database

You've got your @Entity and its @Dao, now it's time to create the actual @Database class. Here's a simple example, using the documentation's User entity and DAO:

@Database(entities = {User.class}, version = 1) //Entities listed here
public abstract class AppDatabase extends RoomDatabase {
    public abstract UserDao userDao(); //Getters for Dao
}

To create a Database class you:

The code to construct the database looks like this:

AppDatabase database = Room.databaseBuilder(getApplicationContext(),
        AppDatabase.class, "database-name").build();

Having more than one instance of a database running causes consistency problems, if, for example, you try to read the database with one instance while you're writing with another instance. To make sure that you're only creating one instance of the RoomDatabase, your database class should be a Singleton.

To perform queries on your database, you will need to access the DAOs by calling the method you provided in the RoomDatabase subclass:

List<User> allUsers = database.userDao().getAll();

Your Turn: Create the Sunshine Database

Complete the following steps to create the SunshineDatabase:

  1. In the data.database package make a new class called SunshineDatabase.java.
  2. Make SunshineDatabase an abstract class that extends the RoomDatabase.
  3. Annotate the class with @Database
  4. Add the version and entities properties to @Database: Add the entities property to your database and put your WeatherEntry.class as the value. Also add the version property and set its' value to 1:
@Database(entities = {WeatherEntry.class}, version = 1)
  1. Add an abstract method for your WeatherDao: Add an abstract method named weatherDao that returns an instance of your WeatherDao:
public abstract WeatherDao weatherDao();
  1. Make SunshineDatabase a Singleton: You can see an example of how you do this in the WeatherNetworkDataSource class, which is another class where you only ever need one instance running. You create a static variable of type SunshineDatabase called sInstance and a lock object to ensure thread safety. Then create a static method called getInstance() which returns sInstance if it exists or creates a SunshineDatabase if it doesn't. The getInstance method can be copied from below:
private static final String DATABASE_NAME = "weather";

// For Singleton instantiation
private static final Object LOCK = new Object();
private static volatile SunshineDatabase sInstance;

public static SunshineDatabase getInstance(Context context) {
    if (sInstance == null) {
        synchronized (LOCK) {
           if (sInstance == null) {
                sInstance = Room.databaseBuilder(context.getApplicationContext(),
                    SunshineDatabase.class, SunshineDatabase.DATABASE_NAME).build();
            }
        }
    }
    return sInstance;
}

Your database is an abstract class that extends RoomDatabase. It is annotated with @Database, which is where it defines its entities. It then has abstract getters for each of your DAOs. It is also a singleton.

TypeConverters

The WeatherEntry class has a java.util.Date object, but you can't store that in a database as is because SQLite doesn't have a Date datatype. To convert it into a type that can go into the database, you'll need a TypeConverter.

To convert between Java types and SQLite supported data types, you'll need to define methods for the conversion and tell Room about them via annotations. Specifically you:

Your Turn: Implement a TypeConverter

  1. Uncomment data.database.DateConverter.java. The code has been written for you in the class. It contains two methods annotated with @TypeConverter which implement the type conversion from Date to long and then from long to Date.
  1. Add TypeConverters to SunshineDatabase: Once you have these annotated converters methods you need to make your SunshineDatabase aware of the converter class. You do this by annotating the database with @TypeConverters and adding the classes that contain @TypeConverter methods:
@Database(entities = {WeatherEntry.class}, version = 1)
@TypeConverters(DateConverter.class)
public abstract class SunshineDatabase extends RoomDatabase { ...
  1. Run your code to make sure no obvious errors have been made.

Your Turn: Room compile time validation

Now that you have added the DAO to the database, you can witness one of the powerful features of Room: compile time validation for your SQLite code.

  1. Change the getWeatherByDate method in your WeatherDAO so that there is a spelling error -- instead of saying "date = :date" have it say "date = :data"
@Query("SELECT * FROM weather WHERE data = :date")
WeatherEntry getWeatherByDate(Date date);
  1. Run your code. The app will not run. You should see a helpful error message if the rest of your database was set up correctly:

The code diff for this step can be viewed here.

Next Steps

You've made a database for Sunshine along with a DAO to access that database. You're now going to create the DetailActivityViewModel and LiveData.

ViewModels

The ViewModel class is designed to hold and manage UI-related data in a life-cycle conscious way. This allows data to survive configuration changes such as screen rotations. By separating out the UI data from the UI controllers, you can create a separation of responsibilities: ViewModels deal with providing, manipulating and storing UI state and UI Controllers deal with displaying the state.

ViewModels are usually associated with a UI controller that they provide data for. They do this using the LifecycleOwner and Lifecycle classes:

When you get a ViewModel, you need to provide a component with a LifecycleOwner. This will usually be an activity or fragment. By providing a LifecycleOwner, you establish the connection between the ViewModel and the LifecycleOwner.

The ViewModel Lifecycle

ViewModels have a different lifecycle scope from their associated UI Controllers. This is because UI Controllers are destroyed and recreated on configuration changes. ViewModels, on the other hand, are not.

Below is a diagram showing how the lifecycle of a ViewModel compares to the lifecycle of the activity, when the activity is created, rotated, then finished.

A ViewModel continues to exist until it's associated UI controller, in this case an activity, is completely destroyed. For a further discussion and a simple example of ViewModels, you can read this blog post.

ViewModels often contain LiveData objects; we'll discuss this relationship shortly and with an example.

Your Turn: The DetailActivityViewModel

In this section you'll uncomment and copy code to see how the connection between ViewModel, an activity and LiveData works. Then you'll get to fully write the code yourself when you work on the MainActivity.

The DetailActivity displays one day's weather forecast. This has a single piece of UI state data associated with it: a WeatherEntry.

  1. Open the build.gradle file for your app or module and add the following dependencies for the Lifecycle libraries:
compile "android.arch.lifecycle:runtime:1.0.0-alpha9"
compile "android.arch.lifecycle:extensions:1.0.0-alpha9"
annotationProcessor "android.arch.lifecycle:compiler:1.0.0-alpha9"
  1. Sync gradle.
  2. Open ui.detail.DetailActivityViewModel and uncomment the entire file. It looks like this:
public class DetailActivityViewModel extends ViewModel {

   // Weather forecast the user is looking at
   private WeatherEntry mWeather;

   public DetailActivityViewModel() {
       
   }

   public WeatherEntry getWeather() {
       return mWeather;
   }

   public void setWeather(WeatherEntry weatherEntry) {
       mWeather = weatherEntry;
   }
}

This class extends ViewModel, giving it the lifecycle scope of the ViewModel. The single WeatherEntry object contains all the data the app needs to display DetailActivity.

  1. Open ui.detail.DetailActivity. Have DetailActivity extend LifecycleActivity instead of AppCompatActivity.
  2. In DetailActivity add a DetailActivityViewModel variable called mViewModel
  3. Add the following line to DetailActivity's onCreate:
mViewModel = ViewModelProviders.of(this).get(DetailActivityViewModel.class);

The first time DetailActivity is created, the ViewModelProviders.of method is called in onCreate. It creates a new DetailActivityViewModel instance.

Then, if a configuration change occurs and the activity is recreated, the ViewModelProviders.of method is called in onCreate again. This time it will return the pre-existing ViewModel instance associated with the DetailActivity.

You could then call mViewModel.getWeather() to get access to the data, which would be preserved regardless of configuration changes.

You get even more benefits when you combine ViewModels with LiveData.

LiveData

LiveData is a data holder class that is lifecycle aware. It keeps a value and allows this value to be observed.

It is a data holder because it literally holds some data. For example:

MutableLiveData<String> name = new MutableLiveData<String>();
name.setValue("Lyla");

This is a LiveData instance that is holding a String object and its current value is "Lyla".

Observation refers to the observer pattern, which is when an object, called the subject, has a list of associated objects, called observers. When the subject's state changes, it notifies all of the observers, usually by calling one of their methods.

In the case of LiveData, the subject is the LiveData and the observers are objects which are subclasses of the Observer class. Whenever setValue is called and the subject's state changes, it will trigger the active observers.

LiveData keeps a list of associated observers and LifecycleOwners. LifecycleOwners are usually activities or fragments. In general Observers are only considered active when their associated LifecycleOwner is on screen. This means it is in the STARTED or RESUMED state. The fact that LiveData keeps track of LifecycleOwners is why LiveData is called lifecycle aware.

The general way to create an observer for a LiveData object is this:

name.observe(<LIFECYCLE OWNER>, newName -> {
   // Do something when the observer is triggered; usually updating the UI
});

A LifecycleOwner is passed into the observe method. This is the LifecycleOwner that is associated with the Observer.

Your Turn: Add LiveData

DetailActivity will observe a MutableLiveData. This MutableLiveData will hold a WeatherEntry. When the MutableLiveData is updated with postValue(), the DetailActivity's Observer will be notified. The DetailActivity will then update the UI.

Do the following to the create and observe your first LiveData:

  1. In DetailActivityViewModel, change mWeather from a WeatherEntry to MutableLiveData<WeatherEntry>. "Mutable" LiveData can change.
  2. In DetailActivityViewModel, initialize your new MutableLiveData<WeatherEntry> object in the constructor.
public DetailActivityViewModel() {
    mWeather = new MutableLiveData<>();
}
  1. In DetailActivityViewModel, update the getter for mWeather to return the new MutableLiveData object.
  2. In DetailActivityViewModel, change setWeather() to be mWeather.postValue(weatherEntry);
  3. In DetailActivity's onCreate, observe the LiveData:
mViewModel.getWeather().observe(this, weatherEntry -> {
   // Update the UI
});
  1. In the new observer, update the different UI elements. There's a bindWeatherToUI() method which takes a WeatherEntry for just this purpose:
if (weatherEntry != null) bindWeatherToUI(weatherEntry);

The last step means that whenever mViewModel.setWeather() is called, the MutableLiveData's postValue() method will be called. postValue() triggers all observers observing the LiveData. In this case there is one observer watching the LiveData, it's the one you just created that updates the DetailActivity's UI.

So in short, when the DetailActivityViewModel's setWeather method is called, this will trigger the UI to update.

See the LiveData and ViewModel in Action

To see how updating DetailActivityViewModel.mWeather immediately affects the UI, add the following code to DetailActivitiy's onCreate():

AppExecutors.getInstance().diskIO().execute(()-> {
   try {

       // Pretend this is the network loading data
       Thread.sleep(4000);
       Date today = SunshineDateUtils.getNormalizedUtcDateForToday();
       WeatherEntry pretendWeatherFromDatabase = new WeatherEntry(1, 210, today,88.0,99.0,71,1030, 74, 5);
       mViewModel.setWeather(pretendWeatherFromDatabase);

       Thread.sleep(2000);
       pretendWeatherFromDatabase = new WeatherEntry(1, 952, today,50.0,60.0,46,1044, 70, 100);
       mViewModel.setWeather(pretendWeatherFromDatabase);

   } catch (InterruptedException e) {
       e.printStackTrace();
   }
});

This is a quick and dirty way of simulating something like a network request or database call asynchronously changing the weather data. The code starts another thread which waits four seconds, calls postValue() with an updated WeatherEntry, waits two more seconds, then calls postValue() again with a different WeatherEntry. The observer relationship between DetailActivity and the LiveData is what updates DetailActivity's UI when the LiveData changes.

If you rotate your phone, you'll notice the same WeatherEntry is shown. This is because the DetailActivityViewModel restores the connection to the same LiveData object, which survives the configuration change because it is stored in the ViewModel.

The code diff for this step can be viewed here.

At this point, you've used Room, ViewModel and LiveData, which encompass all the libraries you set out to learn. But you haven't seen how they work together just yet. The UI is completely separate from the network and from the database you just created. What you need to do now is expose the network and database data to the UI. This is the job of a repository class.

The Repository class

Repository classes are responsible for handling data operations. They provide a clean API to the rest of the app for app data. They know where to get the data from and what API calls to make when data is updated. They are mediators between different data sources (persistent model, web service, cache, etc.).

Unlike the Room, LiveData or ViewModels, the repository class you'll create does not extend or implement an Architecture Components library. It is simply one way of organizing data in your app that is described in the Guide to App architecture.

In your case, the repository class will manage data communications between your newly created WeatherDao, which gives you access to everything in the database, and your WeatherNetworkDataSource, which controls Service classes that pull data from our mock server.

Nothing besides the repository class will communicate directly with the database or network packages, and the data and network packages will not communicate with classes outside of their respective packages. Thus the repository will be the UI's API for getting data to display on the screens.

The General Idea

Downloading weather data and saving it in the database will involve a few classes working together, each performing different functions:

SunshineRepository: Orchestrates all data related commands, delegating to the WeatherNetworkDataSource and the WeatherDao. Observes the WeatherNetworkDataSource to see when it finishes fetching data so that it knows when to update the database.

WeatherNetworkDataSource: Performs all network operations. Provides the source of truth for the most recently downloaded network data. Does this by containing a LiveData object that stores the most recently downloaded data which it updates whenever a fetch is successfully performed.

SunshineSyncIntentService: You'll use an IntentService to perform the actual sync so that if the application is closed, the service will have additional time to finish up the download and database save.

WeatherDao: Used to perform all database operations on the weather table.

Here is how a network sync will be triggered in four parts:

Observation

  1. SunshineRepository: Observes a LiveData provided by WeatherNetworkDataSource.

Start the Service

  1. SunshineRepository: Check if there is enough data, if not...
  2. WeatherNetworkDataSource: Create and immediately start SunshineSyncIntentService.

Do a Fetch

  1. SunshineSyncIntentService: Get an instance of WeatherNetworkDataSource and use it to start the fetch.
  2. WeatherNetworkDataSource: Do the actual fetch of weather data, delegating out to OpenWeatherJsonParser and NetworkUtils. Once finished, post the updated value to the LiveData that stores the most recently downloaded data.

Save in the Database

  1. Finally since SunshineRepository is observing the LiveData, SunshineRepository will update the database

Now you'll write the code for triggering and completing a sync. Once you have this working, you'll add the code to check whether the sync is actually needed.

Your Turn: Set Up Repository

  1. So that you don't need to copy and paste, there is already a SunshineRepository.java skeleton class for you. Uncomment this code. It contains:

    A constructor and getInstance() method. Like the SunshineDatabase, the SunshineRepository is a singleton.

    Empty methods for initializeData(), deleteOldData(), isFetchNeeded() and startFetchWeatherService()

Your Turn: Set Up LiveData

You will use a LiveData variable to store the most recently downloaded data from the network.

  1. In WeatherNetworkDataSource, create a MutableLiveData member variable called mDownloadedWeatherForecasts. It should be private and store an array of WeatherEntry objects because this is what is returned by a data sync operation.
// LiveData storing the latest downloaded weather forecasts
private final MutableLiveData<WeatherEntry[]> mDownloadedWeatherForecasts;
  1. In WeatherNetworkDataSource's constructor, instantiate mDownloadedWeatherForecasts.
mDownloadedWeatherForecasts = new MutableLiveData<WeatherEntry[]>();
  1. In WeatherNetworkDataSource create a getter method for mDownloadedWeatherForecasts called getCurrentWeatherForecasts.
public LiveData<WeatherEntry[]> getCurrentWeatherForecasts() {
    return mDownloadedWeatherForecasts;
}

Your Turn: Start the Service

Now it's time to write the code which starts the IntentService.

  1. In SunshineRepository, complete the startFetchWeatherService() method. Have it call startFetchWeatherService() from WeatherNetworkDataSource which creates and starts the IntentService.
private void startFetchWeatherService() {
    mWeatherNetworkDataSource.startFetchWeatherService();
}
  1. In SunshineRepository, add to the initalizeData() method. initializeData() will be called when the ViewModel requests data. For now, call startFetchWeatherService(). In the future you'll add a check to see if Sunshine needs to start a sync or not.
public synchronized void initializeData() {

    // Only perform initialization once per app lifetime. If initialization has already been
    // performed, we have nothing to do in this method.
    if (mInitialized) return;
    mInitialized = true;

    startFetchWeatherService();
}

Your Turn: Complete the logic for fetching data

Now that you have a service running, let's have that service fetch the data and save it in the mDownloadedWeatherForecasts LiveData.

  1. In InjectorUtils,uncomment provideRepository() and provideNetworkDataSource(). The purpose of InjectorUtils is to provide static methods for dependency injection.

    Dependency injection is the idea that you should make required components available for a class, instead of creating them within the class itself. An example of how the Sunshine code does this is that instead of constructing the WeatherNetworkDatasource within the SunshineRepository, the WeatherNetworkDatasource is created via InjectorUtilis and passed into the SunshineRepository constructor. One of the benefits of this is that components are easier to replace when you're testing. You can learn more about dependency injection here. For now, know that the methods in InjectorUtils create the classes you need, so they can be passed into constructors.
  2. In SunshineSyncIntentService, in onHandleIntent(), call InjectorUtils .provideNetworkDataSource to get a reference to WeatherNetworkDataSource.
@Override
protected void onHandleIntent(Intent intent) {
    Log.d(LOG_TAG, "Intent service started");
    WeatherNetworkDataSource networkDataSource = InjectorUtils.provideNetworkDataSource(this.getApplicationContext());
    
}
  1. In SunshineSyncIntertService, in onHandleIntent(), call the WeatherNetworkDataSource's fetchWeather() method.
networkDataSource.fetchWeather();
  1. At the end of WeatherNetworkDataSource's fetchWeather() method, update the value held in mDownloadedWeatherForecasts with the new forecasts. You should do this using postValue() since the call will be done off of the main thread.
mDownloadedWeatherForecasts.postValue(response.getWeatherForecast());

Your Turn: Observe the LiveData

When initalizeData is called, it starts off a chain of events which spawns a SunshineSyncIntentService which starts a sync and saves the resulting data to mDownloadedWeatherForecasts. The last step is to have the SunshineRepository observe mDownloadedWeatherForecasts and update the database.

  1. In SunshineRepository's constructor get mDownloadedWeatherForecasts. Get mDownloadedWeatherForecasts using the getCurrentWeatherForecasts method you wrote. This is much like the activity using a getter to get and observe a LiveData from the ViewModel.
LiveData<WeatherEntry[]> networkData = mWeatherNetworkDataSource.getCurrentWeatherForecasts();
  1. In SunshineRepository observes mDownloadedWeatherForecasts. In SunshineRepository's constructor, use the observeForever method to observe mDownloadedWeatherForecasts.
networkData.observeForever(newForecastsFromNetwork -> {

});
  1. When mDownloadedWeatherForecasts changes, trigger a database save. In the observer in SunshineRepository call WeatherDao's bulkInsert()method. Note that database operations must be done off of the main thread. Use your AppExecutor's disk I/O executor to provide the appropriate thread:
networkData.observeForever(newForecastsFromNetwork -> {
        mExecutors.diskIO().execute(() -> {
            // Insert our new weather data into Sunshine's database
            mWeatherDao.bulkInsert(newForecastsFromNetwork);
            Log.d(LOG_TAG, "New values inserted");
        });
});

If you want to run your code (always a good idea), call the following from the DetailActivity's onCreate:

// THIS IS JUST TO RUN THE CODE; REPOSITORY SHOULD NEVER BE CREATED IN
// DETAILACTIVITY
InjectorUtils.provideRepository(this).initializeData();

You should see log output like this when run the first time:

  1. All the commands run when InjectorUtils.provideRepository is called in DetailActivity. Note that this is not the proper place to call initializeData() and is just to illustrate what happens when a sync occurs.
  2. When SunshineData.intializeData() is called
  3. onHandleIntent(), including when SunshineSyncIntentService calls InjectorUtils.provideWeatherNetworkDataSource(). Notice how it does not make a new network data source and instead gets the static singleton.
  4. Shows WeatherNetworkDataSource.fetchWeather() running.
  5. Shows when SunshineRepository's observer is triggered and it puts the data into the database.

Note that DetailActivity is not the appropriate place to interact with the SunshineRepository; activities and other UI Controllers should never directly interact with the repository. That's a job for the ViewModel; you'll be working on that next.

The code diff for this step can be viewed here.

At this point you've performed a sync with the server. Through the power of observation, your repository automatically updates your database. Now you need to have the ViewModel get the weather data from the repository.

Your Turn: Expose WeatherEntry by Date

Your DetailActivityViewModel needs the data from the database, namely, it needs the weather information for one day as a LiveData object. In the WeatherDao, you have a method getWeatherByDate() which returns a WeatherEntry. This is close to perfect.

Room has an extremely handy feature for when you want to have a LiveData object that stays in sync with whatever is in the database - Room can return LiveData wrapped objects. This LiveData will trigger its' observers any time the database data changes. It even loads this database data off of the main thread for you.

Get the LiveData from Room by following these steps:

  1. In WeatherDao, update getWeatherbyDate() to return LiveData<WeatherEntry>.

The WeatherDao returns the data you want and you could have the DetailActivityViewModel communicate directly with the WeahterDao. But that defeats the whole point of the repository class; the repository should be the single source of truth for all data operations. Therefore the DetailActivityViewModel will get this data from the SunshineRepository. The SunshineRepository will, in turn, ask the WeatherDao for the LiveData:

  1. In SunshineRepository add the getWeatherbyDate() method. This method should take in a Date object and return LiveData<WeatherEntry>. It should use the WeatherDao object stored in the SunshineRepository to get the LiveData object.
  2. If you haven't already, remove the following from DetailActivity's onCreate:
// THIS IS JUST TO RUN THE CODE; REPOSITORY SHOULD NEVER BE CREATED IN
// DETAILACTIVITY
InjectorUtils.provideRepository(this).initializeData();
  1. In SunshineRepository.getWeatherByDate call initializeData(). You'll do "lazy" instantiation of our data - when it's requested, you'll load from the network. This shows off one helpful aspect of the repository: Since it is the API through which all data requests are made, you ensure that every time you getWeatherByDate(), the data initialization is triggered. This would not be possible if you directly accessed the WeatherDao.

    You can change the access modifier on initalizeData() to private since it now is only used within the repository.

ViewModelProvider Factories

Next you need to use the repository to get the data. There's one problem, though, the ViewModel doesn't have a reference to a SunshineRepository.

The most testable way to design the code is to pass an instance of the SunshineRepository into the DetailActivityViewModel - this would allow you to easily mock the repository when you're testing the view model.

The constructor that is automatically called by ViewModelProvider is the default one - it takes no arguments. If you want to create a different constructor for a view model, you'll need to make a view model provider factory. The starting code can be found if you uncomment DetailViewModelFactory:

public class DetailViewModelFactory extends ViewModelProvider.NewInstanceFactory {

   private final SunshineRepository mRepository;

   public DetailViewModelFactory(SunshineRepository repository) {
       this.mRepository = repository;
   }

   @Override
   public <T extends ViewModel> T create(Class<T> modelClass) {
       //noinspection unchecked
       return (T) new DetailActivityViewModel(mRepository);
   }
}

To create a view model provider factory you must:

  1. Extend ViewModelProvider.NewInstanceFactory
  2. Take in whatever constructor parameters you want in the DetailViewModelFactory. In this case you're taking in the repository.
  3. Override the create() method which calls your custom view model constructor

To then use the factory, you'd call

// Get the ViewModel from the factory
DetailViewModelFactory factory = new DetailViewModelFactory(repository);

mViewModel = ViewModelProviders.of(this, factory).get(DetailActivityViewModel.class);

Your Turn: Create DetailViewModelFactory

You'll finish writing the DetailViewModelFactory and use the InjectorUtils class to create it. The skeleton code is there for you, but you'll also want to pass in a Date along with the repository:

  1. In DetailActivityViewModel change the constructor so that it takes both a SunshineRepository a and a java.util.Date:
public DetailActivityViewModel(SunshineRepository repository, Date date)
  1. Uncomment the DetailViewModelFactory skeleton code if you haven't already.
  2. In DetailViewModelFactory, add the ability to pass in a Date to the constructor. Use how you passed in the repository as reference.
  3. In InjectorUtils, uncomment provideDetailViewModelFactory. The constructor signature it uses should match with the new parameter you added.
  4. In DetailActivity 's onCreate() make a Date for today using the following code:
Date date = SunshineDateUtils.getNormalizedUtcDateForToday();
  1. In DetailActivity, get a reference to DetailViewModelFactory using InjectorUtils.provideDetailViewModelFactory().
  2. In DetailActivity, use the DetailViewModelFactory to get an access to view model:
ViewModelProviders.of(this, factory).get(DetailActivityViewModel.class);

Your Turn: Get the WeatherEntry by date

Now that you have access to the Date and SunshineRepository:

  1. In the DetailActivityViewModel, get the weather entry LiveData from the repository, using the date.
  2. Cleanup the code. You'll need to cleanup a few things:
  1. First, the LiveData in your view model will no longer be modified by your app. Change it from a MutableLiveData to a LiveData.
  2. Similarly remove the setWeather() method, as you can no longer use it.
  3. In the DetailActivity, remove the code which simulated a network request using thread.sleep().
  4. If there is still a call to initalizeData in the activity or any direct communication with the SunshineRepository, remove it!

Run your code, you should see randomized weather data.

The code diff for this step can be viewed here.

Sunshine is now doing a basic load of data from the network, saving it in the database, and displaying it. There are two buggy inefficiencies you should fix up before moving on to any new functionality:

  1. The DetailActivityViewModel is requerying the network every time it's created. Before you even start the SunshineSyncIntentService, you should check what's already in the local cache. After all, the point of the local cache is to avoid needlessly re-downloading the data. This also showcases how SunshineRepository mediates and orchestrates data flow in the app - a complete sync will include checking the DAO to see if there is data, then if not, performing the network sync, then finally updating the DAO.
  2. The app isn't built to display historical weather data, only future weather data. Yes, there's no process to delete old data! If your user loves Sunshine and uses it for a year, there will be 365 of uselessly stored historical weather data on the user's phone.

Your Turn: Fetch when needed

There are a number of different ways you could decide whether to download data. For Sunshine, you will do the following:

  1. Count how many days of past the current date are in the database
  2. If this is less than two weeks (14 days), download more data

We're using two weeks because that is how much data you want to display in the MainActivity, which you'll be working on next. To implement this:

  1. In WeatherDao, create the method signature for countAllFutureWeather. This should be a query method that uses the SQL COUNT command to get a list of how many future dates of weather there are.
  2. In SunshineRepository finish the isFetchNeeded() method. This should check whether you have at least 14 days of weather and return true if there is less than 14 days of weather.
  3. In SunshineRepository's initalizeData() method, use the isFetchNeeded() method to determine whether to start the SunshineSyncIntentService. You'll want to be on the disk I/O thread to do this:
mExecutors.diskIO().execute(() -> {//CODE ON DISK I/O THREAD HERE});

You should now notice when you re-open the app after running it for the first time, it will not grab new randomized weather data.

Your Turn: Delete Old Data

Now let's delete the old, outdated, data:

  1. In WeatherDao, create the method signature for deleteOldData(). This method should delete all dates before a given date. While the name includes "delete" you'll want to use the @Query annotation and not the @Delete annotation. This is because you need to write some SQL to define the WHERE clause.
  2. In SunshineRepository, finish the deleteOldData() method. You'll need to get the current date for deleteOldData(), you can use the same code you use in isFetchNeeded():
Date today = SunshineDateUtils.getNormalizedUtcDateForToday();
  1. In SunshineRepository's network data observer, delete the old weather forecasts before inserting new data. Call deleteOldData().

This will delete all old data whenever you do a save to the database. Because the app uses the OnConflictReplace strategy and has ensured uniqueness by date, if it gets new weather information it'll also update what's already in the database.

The code diff for this step can be viewed here.

Like the DetailActivity, Sunshine has all the UI built for a functioning RecyclerView screen called MainActivity. In this code step, you'll apply what you've learned to create a ViewModel, ViewModel provider factory and the correct LiveData for MainActivity.

Since you've done many of these steps once, this is your chance to apply what you've learned:

The code diff for this step can be viewed here.

The code diff for this step can be viewed here.

The code diff for this step can be viewed here.