Android Architecture Components — Make your dream app!

.
12 min readJun 2, 2018

--

Start planning your dream app!

1. Intro to Android Architecture
As Android developers, we usually need to persist data into a local database, mange lifecycle to handle rotation, make our apps modular so our code is easy to maintain and reuse and prevent common errors like memory leaks and reduce the amount of boilerplate that we need to write over and over.

Android Architecture components were created to facilitate all this common challenges.

  • Room — A new SQLite Object Relational Mapping library
  • LiveData — Observe changes in the database
  • ViewModel — Cache data that needs to survive through configuration changes.
  • LifeCycle — Allow non-lifecycle objects to be lifecycle aware

2. Intro to Room
Content Providers are one of the four main components of Android applications. Content Providers have some nice advantages that we may want to include in our app. For those cases in which we will not benefit from any of those advantages, rom comes to play.

Rom is an ORM, Object Relational Mapping library. In other words, Room will map our database objects to Java objects.

Room Features:
(1) Write less boilerplate
(2) Benefit SQL validation at compile time
(3) Built to work with LiveData and RxJava for data observation

Room annotations and main components:
(1) @Entity — Define our database tables
(2) @DAO — Provide an API for reading and writing data
(3) @Database — Represent a database holder

Creating an Entity
This will include a list of entities and @DAOs that will allow us to create a new database or to acquire connection to our database at runtime.

Android Architecture components is a set of libraries we should add the relevant dependencies to our build gradle file.

Each POJO, Plain Old Java Object, may become to an entity by adding the @Entity annotation before the declaration of our class. We can change the name of the table by using @Entity (taskName =”new_table_name”).

When we use @Entity we should define a @PrimaryKey. Sometime, when we want to auto generate our id we would use @PrimaryKey(autogenerate = true).

One of Room limitations is that it can use only one constructor so if we have more than one we need to add @ignore to the other constructors.

Creating a DAO
DAO is a Data Access Object. DAOs should be created for each of our entities.

DAO is an interface that has the @Dao annotation.

In the DAO we should have a method declarations for each of the CRUD operations we can use with the right annotation (@Query, @Insert, @Update, @Delete).

Example for querying tasks order by their priority:
@Querty(“SELECT * FROM task ORDER BY priority”)
List<TaskEntry> loadAllTasks();

This query will return a list of TaskEntry objects.

The fact that we can request objects back is what makes room an ORM.

We can use DAO as also replace an entry in case of conflict by using the following annotation:
@Update (onConflict = OnConflictStrategy.REPLACE)

Creating a Database
To create a database we need to define an abstract class that extends roomDatabase.

In the class there will be a static method called getInstance() what will return our class using the Singleton pattern. We want to ensure only one database object is created.

The instance of our class will be saved in a static variable named sInstance. This variable will get his value by using Room.databaseBuilder() in our getInstance() method.

This method will create the database if it does not yet exist yet, otherwise it will create a database connection to the already existing database.

We need to add the @Database annotation with the values of its entities, version and exportScema.

To add our database DOA we need to include an abstract method that returns it class.
Type Converters
If room cannot figure out how to save one of the fields (like date) into our database we should use @TypeConverter.

Date values in a database should be store only as text, real or integer.

Room needs to map each of our fields to one of the database data types. Room cannot automatically map more complex extractors like dates.

We should create a new class that will have two methods that will be annotated with @TypeConverter. The first should convert data from the database to or code (Ex: Long → Data), and the second should convert the type from our code to a readable type of the database (Ex: Date → Long).

We should add another annotation of @TypeConverters() with the name of our converter class in the abstract database class we created. The annotation should be written above the class declaration.

Insert new entry
All database operations should NOT be made on the main thread.

We can make them work on our main thread temporarily only to check if our implementation of Room works as expected. To do that, in out database class and enable allowMainThreadQueries() on the Room.databaseBuilder().

In our AddNewEntry class we need to add a member variable of our database and initialize it in onCreate() with the application context.

We can take the data from the user input to create our new entry, using its constructor. Then, we will retrieve the DAO from our database instance and call the insert() method passing the new entry we created.

Query entries
To query our data from the database we should create a member variable of our database in our activity and initialize it in in onCreate() with the application context.

We should query the database in onResume(), so each time we come back to our activity we will see the most updated list.

We can do it by updating the adapter with the new data taken from our query() method in our DOA class.

Executors
To run our database operations not on the main thread we need to run them in separate threads. Once the thread is done it will be garbage collected. This process will be repeated many times.

We should aim to run all our database calls in the same thread to avoid problems, and ensure that our calls are done sequentially.

An Executor is a way to do it.
An Executor is a object that executes a submitted runnable tasks. It is normally used instead of explicitly creating threads for each of a set of tasks.

We need to make an Executors class in our code, that will be created as a singleton. This class should contain 3 executors:
(1) diskIO — Single thread executor, it ensures that our database transactions are done in order.
(2) networkIO — Pool of 3 threads which allows us to run different network calls simultaneously controlling the number of threads that we have.
(3) mainThread — Uses the mainThread executor class which essentially will post runnables using a handle associated with the main looper. When we are in an activity we do not need this mainThread executor because we can use the runOnUIThread() method. When we are on a different class and we do not have the runOnUIThread() method we can access the main thread using this last executor.

To use our executor class and use its diskIO executor, we should call it from every place we make a database operation and put the operation in the executor runnable.

Delete entry
When we want to delete a single entry we need to find out which one we want to delete.

We can find that data in our adapter. In order to access our list of entries we will need to add a public getter in our adapter.

To get the exact entry we need to know which position was chosen by the user. In the main activity we can get that information by calling the getAdapterPosition() method of our viewHolder.

Then we can call the delete() method in our DAO passing the right entry.

After deleting the entry from our database we should remember to update our UI as well as we did in the onResume() method before.

Query a single entry
When we want to query a single entry we need to add a new method in our DOA class.

The class should have the @Query annotation and use the where clause in the following way:
@Query(“SELECT * FROM task WHERE id = :id”)
TaskEntry loadTaskById(int id);

By prefixing with a colon the parameter with the same name it will be automatically used.

If our query is wrong from some reason, we will get an error at compilation time, thanks to Room validation.

Update an entry
We can update the entry by passing the id of the entry we want to change from one activity to the other using intent putExtra() method.

On the other Activity we can get the id from the intent and query the right entry from our database. We will do it by using the executor class and the diskIO executor.

We can not modify the UI from the executor’s runnable so we will add there a runOnUIThead() method and make the variable we would like to pass as final.

3. Intro to LiveData
LiveData is an observable data holder class.Using liveData makes our app more efficient by not calling the query() method for no reason.

Unlike a regular observable, LiveData is lifecycle aware, meaning it respects the lifecycle of other app components. This awareness ensues LiveData only updates app component observers that are in an active lifecycle state.

LiveData sits between our database and our UI. LiveData is able to monitor changes in the database and notify the observers when data changes.
UI <-(Notify)-(Observe)-> Live Data ← Monitor → Database

This is possible thanks to the observer pattern. The clsasses called Observers subscribe to a subject. The subject, which in our case is the LiveData object, will keep a list of all the Observers that are subscribed to it and notify all of them when there is any relevant change.

When our data changes, the setValue() method on our LiveData object will be called. That will trigger a call to a method in each of the Observers. Then the Observers will do whatever they are supposed to do when that happens, like updating the UI with the up to date data.

We can add LiveData by adding the right gradle dependencies and annotationProcessor first.

Then, we can open our DAO interface and change the query() method to return LiveData<OUR_OLD_OBJECT> . We should change the type to LiveData<OUR_OLD_OBJECT> any place we call the querry() method from the DAO interface as well.

Then we can call it’s observe method, passing this as the lifeCycleOwner and a new Observer. The new Observer is an interface where we need to implement onChanged() method and put their thelogic that was in our executor runnable. The logic to update the UI is now in the onChanged() method of the observer which runs on the main thread by default.

LiveData will run by default outside of the main thread, because of that, we can avoid using the executor.

We can do this for query() because LiveData observe the changes in the database. For other operations as: insert(), update() and delete() we do not need to observe changes in the database. For those operations we will not use LiveData and will keep on using the executors.

Receiving the data for the first time and every change made in our database will trigger the onChanged() method of the observer.

Therefore, we will no longer need call another update to our UI when we delete a new entry or in onResume(), we can just make it once in onCreate() and all other changes will happen by the observer.

In any case that we do not want to receive updates, like when we are querying a single entry, we will need to use the removeObserver() method and passing the activity.

4. Intro to ViewModel
We are still re-querying the database every time we rotate our device, because our activity is destroyed and recreated on rotation. A new LiveData object will be created and for that there will be a new call to the database. To avoid that we can use ViewModel.

The ViewModel class is designed to store and manage UI-related data in a lifecycle conscious way. ViewModel allows data to survive in configuration changes such as screen rotations.

The lifecycle of the ViewModel starts once the activity is created and lasts until it is finished. Because of that we can cache complex data in our ViewModel.

When the activity is recreated after rotation, it will use the exact same ViewModel object where we already have our cache data.

It is common that we need to make some async calls to retrieve our data. Without the ViewModel, if the activity is destroyed before the call finishes, we could have memory leaks. Instead, ifd we make this async calls from the ViewModel, the result will be delivered back to the ViewModel and it will not matter id the activity has been destroyed or not — There will be no memory leaks that we need to worry about.

To add a ViewModel we need to create a new ViewModel class to our activity that will extends AndoidViewModel.

Our constructor will receive a parameter of type application, and inside of it we will call super() to its parent class.

We will create a private member variable of type LiveData to cache our list of entries wrapped in a LiveData object. We will initialize it inside the constructor by getting an instance of our database and query all the data from our DAO interface. At the end we will add a getter() method. That’s all.

To use our ViewModel in main activity, we need to remember that we are now querying the database in our ViewModel.

In order to get the ViewMode, we just need to call ViewModelProviders.of() and pass this activity and the ViewModel class we created as parameters.

Now, we can retrieve our LiveData object using the getTasks() method from the ViewModel and call this method in an appropriate name as setupViewModel().

We can add a ViewModel to more than one activity.

To pass a value to the ViewModel we need to use the ViewModel factory by creating a new class that extends from ViewModelProvider.NewInstanceFactory. The class will include 2 member variables of the database and the entry id. In addition, we need to override the create() method to return a new ViewModel that uses our parameters in its constructor.

We need to create a new ViewModel class that uses a factory by creating a new class that extends ViewModel. The class will have a LiveData variable and a public getter. In the constructor we can load the specified entry we want.

In the activity we need to create an instance of our factory by passing the database and the entry id to its constructor. We then need to create our ViewModel instance passing the factory as a parameter. We can retrieve the LiveData we want to observe by calling the getTask() method of our ViewModel. We can also use this method to remove the observer.

A LiveData object is cast in the ViewModel. We can retrieve it again after rotation without any need to re-query the database.

5. Intro to LifeCycle
The flow we usually expect is something like this:
Save in DB → LiveData updated → Observer Notified → UI updated

What happens if we are not in the activity that receives the update?
Save in DB → LiveData updated → (X) Observer Notified → (X) UI updates

The observer should not get notified and the UI shouldn’t get updated.

That’s because activities cannot have their code run in the background.

If LiveData has tried to call the onChange() method of the observer in other activity, that code has not been executed. Therefore, the UI in the other activity has not been updated.

Thankfully, LiveData already support lifecycle which allows us to update the UI in different activity than the one we are in now.

The reason for that is that LiveData is a lifecycle aware component. LiveData is able to know the state of its associated component, as the activity.

If the activity is started or resumed then it consider active and only in that case it’s observer will be notified.

Once we go back to our main activity, like pressing the back button, main activity becomes active again and LiveData notifies it’s observer so the UI is updated.

LiveData will also know when the activity is destroyed, and when that happens it will automatically unsubscribe the observers for us to avoid memory leaks.

Lifecycle is a component that allow non lifecycle objects to be lifecycle aware and that the different components will decide to fit together as building blocks.

Lifecycle has 2 interfaces:
(1) Lifecycle Owner — Objects with a lifecycle, like activities and fragments.
(2) Lifecycle Observers — Observe LifeCycle Owners and get notified on life cycle changes

LiveData is lifecycle aware because it is a Lifecycle Observer. When we call it observe() method we let it know which Lifecycle Owner it should observe by passing it as a parameter.

We can implement our very own Lifecycle Observers if we need.

We can also use a Repository module to add flexibility to our architecture and isolate the code change to one place. Repository modules are responsible for handling date operations.

REFERENCES:

You can find much more at the free course Developing Android Apps by Google on the Udacity website over here: (check it out!)

https://www.udacity.com/course/new-android-fundamentals--ud851

--

--