Categories
android architecture

Android Architecture Components – Looking at Room and LiveData – Part 1

This week at Google I/O 2017, there were a lot of new announcements for the Android Platform. One of the announcements was the new architecture guidelines for Android! This is a welcome addition to the Android platform.

Previously the Android team refrained from giving advice as to how you should structure your Android applications. For the most part this meant that anyone learning Android for the first time would just end up placing all their code into the Activity files and occasionally moving stuff into an AsyncTask if the app crashed with a NetworkOnMainThreadException. Only after trying to add unit tests and instrumentation tests would you really understand that your code you have just spent so long developing was not easy to read, make changes to or to write tests for.

Then everyone started talking about different patterns – MVP, MVVM, MVI patterns, articles and libraries. The Android Framework Team has since realised there is a strong need to increase the guidance they provide with regards to best practice architecture for Android apps. These new Architecture Components released by the Android team aim to change this.

What are the new Architecture Components?

The Architecture Components Framework is a set of libraries and guidelines that serve as a basis for writing Android apps. They address common scenarios that developers face across a wide range of applications. This framework aims to reduce the amount of boilerplate and repetitive code, leaving you to focus on the core functionality of your application.

The basic blocks of the architecture components include the following:

  • Room – A SQLite object mapper. Very similar to other libraries such as ORMlite or greenDAO. It uses SQL while still allowing compile time guarantees on the queries.
  • LiveData – A Lifecycle aware observable core component.
  • ViewModel – The communication points with the rest of the application for Activities / Fragments. They are UI code free and outlive the activity or fragment.
  • Lifecycle – A core part of the Architecture components, it contains information about the lifecycle state of a component (for instance an Activity).
  • LifecycleOwner – Core interface for components that have a Lifecycle (Activities, Fragments, Process, your custom component).
  • LifecycleObserver – Specifies what should happen when certain Lifecycle methods are triggered. Creating a LifecycleObserver allows components to be self-contained.

Using the new Architecture Components in an app

We will be building an app that is a countdown to different events that you add to the app. We will be using the MVVM pattern.

Below is a diagram illustrating the Android app we will be building using the new Architecture components. This diagram details the end result of the Date Countdown app we will be building in this series. It also indicates which architecture components are used in which portion of the app.

The main difference between MVP and MVVM is that in MVVM ViewModels expose data and the interested parties can listen to that data (or ignore it) whereas with MVP there is a strict contract between the View and the Presenter. With MVP, it is harder to reuse the Presenters as they are tightly coupled to the View. With MVVM, Views subscribe to the data they are interested in from the ViewModel.

What is Room?

Room is a new way to create databases in your Android apps. Room eliminates a lot of the boilerplate code you had to write previously to store data in your apps. Room is an ORM between Java classes and SQLite. With Room, you no longer need to use Cursors and Loaders. Room isn’t a fully-fledged ORM, for instance, you cannot have complex nesting of objects like other ORM solutions might provide.

With Room there are a few different ways in which you can query data:

  • Use LiveData which is a class that exposes a stream of events that you can subscribe to receive updates for. This can be used on the main thread as it is asynchronous.
  • Use the RxJava2 Flowable abstract class.
  • Place synchronous calls in a background thread such as an AsyncTask. (Room doesn’t allow you to issue database queries on the main thread (as this can produce ANRs)).

Get started with Room

In this example, we will create an app that allows us to save important dates and it will show countdown to each one. The full source code can be found here. This blog post seeks to highlight the use of Room and the other new components but will assume basic knowledge of other Android concepts.

  1. Create a new project in Android Studio with a default empty activity.
  2. Add the Google Maven Repository to the top level build.gradle:
    allprojects {
        repositories {
            maven { url 'https://maven.google.com' }
            jcenter()
        }
    }
  3. Add the Room dependencies to your app/build.gradle:
       compile "android.arch.lifecycle:extensions:1.1.0"
       compile "android.arch.persistence.room:runtime:1.1.0"
       annotationProcessor "android.arch.lifecycle:compiler:1.1.0"
       annotationProcessor "android.arch.persistence.room:compiler:1.1.0-alpha1"

  4. Create an entity called Event. This is the table that will store a list of events a user creates in the app to countdown to. We annotate the class with the @Entity annotation and the name of the table (events in this case). Annotate the id field with @PrimaryKey annotation and the option to autoGenerate the field we can set to true in this case. Room will then create a table automatically with the fields defined in the object.
    @Entity(tableName = TABLE_NAME)
    public class Event {
       public static final String TABLE_NAME = "events";
       public static final String DATE_FIELD = "date";
    
       @PrimaryKey(autoGenerate = true)
       private int id;
       private String name;
       private String description;
       @ColumnInfo(name = DATE_FIELD)
       private LocalDateTime date;
    
       public Event(int id, String name, String description, LocalDateTime date) {
           this.id = id;
           this.name = name;
           this.description = description;
           this.date = date;
       }
    
       public int getId() {
           return id;
       }
    
       public String getName() {
           return name;
       }
    
       public String getDescription() {
           return description;
       }
    
       public LocalDateTime getDate() {
           return date;
       }
    
       @Override
       public String toString() {
           return "Event{" +
                   "id=" + id +
                   ", name='" + name + '\'' +
                   ", description='" + description + '\'' +
                   ", date=" + date +
                   '}';
       }
    
       public long getDaysUntil() {
           return ChronoUnit.DAYS.between(LocalDateTime.now(), getDate());
       }
    }

  5. Create the Data Access Object (or the DAO) by creating an interface called EventDao. Annotate the class with the @Dao annotation. A class implementation will then be generated by Room that implements the methods defined in the interface (very similar to how Retrofit works). We can use different annotations here, annotations such as @Query, @Delete, @Insert, @Update. The @Query annotation can take a SQL structured query. The great part about this is the compile time checking that happens on these scripts. For example: If you type a name of a table incorrectly, Room will not allow you to compile your app until you have corrected this.
    @Dao
    public interface EventDao {
    
       @Query("SELECT * FROM " + Event.TABLE_NAME + " WHERE " + Event.DATE_FIELD + " > :minDate")
       LiveData<List<Event>> getEvents(LocalDateTime minDate);
    
       @Insert(onConflict = REPLACE)
       void addEvent(Event event);
    
       @Delete
       void deleteEvent(Event event);
    
       @Update(onConflict = REPLACE)
       void updateEvent(Event event);
    
    }
  6. Create an abstract class called EventDatabase, this will be where we link up the different entities (or tables) that we wish to create. This class should extend RoomDatabase.
    @Database(entities = {Event.class}, version = 1)
    @TypeConverters(DateTypeConverter.class)
    public abstract class EventDatabase extends RoomDatabase {
    
       public abstract EventDao eventDao();
    
    }

    You will notice the abstract method eventDao() that returns the EventDao we just created. Room returns the correct instance of this class at runtime.

    It is worth noting that the @TypeConverters(DateTypeConverter.class) annotation automatically serializes the LocalDateTime object date into a String format of it and deserializes it back into a LocalDateTime object when it reads it out from storage. Below is the example class definition for the DateTypeConverter class:

    public class DateTypeConverter {
    
        @TypeConverter
        public static LocalDateTime toDate(Long timestamp) {
           //.. convert
        }
    
        @TypeConverter
        public static Long toTimestamp(LocalDateTime date) {
          //.. convert
        }
    }
  7. Create a singleton EventDatabase object that uses Room.databaseBuilder(…) to create an instance. We could also create an in-memory database by using Room.inMemoryDatabaseBuilder(..) method. We can do this easily with Dagger or create the singleton manually. With Dagger, our Module looks as follows (See the full source code for more):
    @Module
    public class CountdownModule {
    
       private CountdownApplication countdownApplication;
    
       public CountdownModule(CountdownApplication countdownApplication) {
           this.countdownApplication = countdownApplication;
       }
    
       @Provides
       Context applicationContext() {
           return countdownApplication;
       }
    
       @Provides
       @Singleton
       EventRepository providesEventRepository(EventDatabase eventDatabase) {
           return new EventRepositoryImpl(eventDatabase);
       }
    
       @Provides
       @Singleton
       EventDatabase providesEventDatabase(Context context) {
           return Room.databaseBuilder(context.getApplicationContext(), EventDatabase.class, "event_db").build();
       }
    }

Now we have our structure in place to add items, query and delete, we can use them and discuss the LiveData class that we used in the EventDao class defined above.

What is LiveData?

LiveData allows you to observe changes to data across multiple components of your app without creating explicit, and rigid dependency paths between them. LiveData will respect the different lifecycles of activities and fragments. When you combine LiveData with Room, it gives you the ability to receive automatic database updates which can be difficult to achieve by just using a standard SQLiteDatabase.

  1. Create an activity called EventListActivity and a fragment called EventListFragment. Inside the activity, inflate the fragment. Make sure your fragment extends Fragment (support library version).
  2. In the fragment, add a RecyclerView, EventAdapter and EventViewHolder, these will be used for displaying our list of events.
  3. In the fragment, we can easily get a reference to the EventDatabase and observe events from the database when new items are added. In the observable callback, we can then set the items on the adapter. We can inject the EventDatabase object using Dagger.
    eventDao = eventDatabase.eventDao();
    
    eventDao.getEvents().observe(this, events -> {
         Log.d(TAG, "Events Changed:" + events);
         adapter.setItems(events);
    });

By passing this as the first parameter, the LiveData observable will be managed automatically for you. This means the fragment will take care of disposing the observables when the fragment is no longer used. The LiveData class is an example of a LifecycleObserver. It automatically stops sending updates when the Lifecycle is in a Lifecycle.State.DESTROYED state and restarts sending updates when the Lifecycle is in a Lifecycle.State.STARTED state.

Add a new event using Room

  1. Create a new fragment that contains two EditText fields, a date picker and a save Button.
  2. The onClickListener for the save button can easily write to the event database by just calling eventDatabase.eventDao().addEvent(). (Note: This should be called from a background thread, in this example I’ve used an RxJava Completable but you can use AsyncTasks if you wish).
    String eventTitle = editTextTitle.getText().toString();
    String eventDescription = editTextDescription.getText().toString();
    Event event = new Event(0, eventTitle, eventDescription, eventDateTime);
    
    eventDatabase.eventDao().addEvent(event); //Run this in a background thread.

We now have access to the database and we can easily insert or query the database from the UI.

In Summary

Room is a easy to use library that wraps the SQLite implementation on Android. It also provides an intuitive interface for dealing with Objects instead of Cursors or ContentProviders. Using Room with LiveData is where the real magic happens. It allows views to be notified about data changes, which can be difficult to achieve with a standard SQLiteDatabase.

There are a few pitfalls with loading data directly in the activity or fragment. The main issue is that your activity or fragment becomes tightly coupled to the database. This is not a good approach if you want to add tests or reuse the logic in another place. It is a much better approach to separate database logic from View logic. The ViewModel Architecture Component aims solve this problem. In the next blog post, we cover using a ViewModel and a ViewModelProvider to structure our countdown app better and follow the diagram that we have detailed at the start.

Find the full source code example using new Android Architecture components.

References

Thanks to Yigit Boyar and Erik Hellman for reviewing this blog post.