4.
Android Architecture Components
Written by Yun Cheng
For many years, Android developers have had to handle database mappings, lifecycle management issues, asynchronous operations and other tricky challenges that were prone to errors and resulted in boilerplate code. In November 2017, Google introduced the Android Architecture Components, a collection of libraries to help with some of these architecture-related pain points. This book uses several of them in the sample project at various points: Room, LiveData, ViewModel and Data binding.
This chapter provides an overview of these libraries and goes into detail about using a subset of these in the sample project. While there’s no sample project for this chapter, you’ll view the sample projects from other chapters to see these libraries in action. For the Android Architecture Components that aren’t covered in the sample project, links to free tutorials are provided where you can learn more.
Room
The Room library handles persistence for your app. Room provides an abstraction layer over SQLite and makes it easy to directly map objects to raw database content, as well as easily define type-safe queries for interacting with data. This is achieved through the use of annotations that generate boilerplate code behind the scenes for you.
Lifecycle
The set of classes in this library offer lifecycle management, allowing you to create components that can automatically adjust their behavior based on the current Android lifecycle state.
To see an example using this library, you can read this tutorial: www.raywenderlich.com/164-android-architecture-components-getting-started.
ViewModel
A ViewModel provides data between a repository and your View. Unlike Activities, which get destroyed on configuration changes, ViewModels survive configuration changes, keeping your data safe and preserving state.
LiveData
If you’ve used RxJava before, LiveData is a similar library (with scaled down features) that allows Android Views to observe model changes and respond accordingly. LiveData is also lifecycle-aware, so it only tells the UI to update things if it’s in the correct lifecycle state. It also gets cleaned up for you, unlike RxJava subscriptions, which you must dispose of yourself.
Data binding
The data binding library allows you to observe changes in the ViewModel directly within the View XML. This save you from having to write the boilerplate code that’s normally required to bind data to Views in code.
Paging
The Paging library helps you to display lists (especially infinite ones) by loading data in small chunks and updating the UI as more data is loaded.
To learn more about this topic, read this tutorial: www.raywenderlich.com/6948-paging-library-for-android-with-kotlin-creating-infinite-lists.
Navigation
The Navigation architecture component allows you to specify navigation throughout your app using an XML graph or via a graph editor that’s built into the latest version of Android Studio. It reduces the hassle of handling it all yourself through boilerplate code.
This tutorial will help get you started using this library: www.raywenderlich.com/6014-the-navigation-architecture-component-tutorial-getting-started.
WorkManager
The WorkManager library manages deferrable background tasks that require guaranteed execution.
Read this tutorial for more information: www.raywenderlich.com/6040-workmanager-tutorial-for-android-getting-started.
Using the Android Architecture Components
Phew! That was a long list of components. Tying them together can be a daunting task, but each one has a very important role to play when talking architecture. In this section, you’ll go over how the WeWatch app makes use of some of these components.
Room
Throughout this book, the sample project uses the Room library for its database operations.
Open the starter project from the MVC chapter 2 and review the code to see how it’s implemented. Specifically, open and review the moving entity in Movie.kt, the data access object in MovieDao.kt, the database class found in LocalDatabase.kt and the data source class found in LocalDataSource.kt.
In WeWatch, the main data object of interest is Movie
, and the app persists a user’s list of movies by storing each movie as a row in a database table. With Room, it’s easy to create a table named movie_table
representing the Movie
class in the database.
For any class whose fields you wish to directly map in a database table, use the @Entity
annotation above the declaration for the class. By default, this creates a table with the same name as the class. You can also give the table a different name by explicitly setting the tableName
, as is done in Movie.kt:
@Entity(tableName = "movie_table")
data class Movie(...) {...}
By default, the columns for this table have the same names as the fields in the constructor for this class, such as id
, title
and releaseDate
. If you want a column to have a different name from the field name in the class, you can use the @ColumnInfo
annotation before a field.
To set the primary key for a table, use the @PrimaryKey
annotation. In the case of the movie table, the primary key is id
:
@PrimaryKey
@SerializedName("id")
@Expose
var id: Int? = null
Note: The
@SerializedName
and@Expose
annotations are from the Gson library, not from Room. The app uses Gson to convert JSON data from API web calls to Kotlin data objects. Thus, this singleMovie
class can fluidly convert between JSON to Kotlin object to Room database entity all through the use of annotations.
Next are the operations on this table, such as retrieval, insertion and deletion. The app accesses these operations through data access objects (DAO). Generally, each table has its own DAO interface, specified using the @Dao
annotation.
Open MovieDao.kt, the DAO for the movie table, and look at the following CRUD (Create, Read, Update, Delete) operations it has:
@Dao
interface MovieDao {
@get:Query("SELECT * FROM movie_table")
val all: Observable<List<Movie>>
@Insert(onConflict = REPLACE)
fun insert(movie: Movie)
@Query("DELETE FROM movie_table WHERE id = :id")
fun delete(id: Int?)
@Query("DELETE FROM movie_table")
fun deleteAll()
@Update
fun update(movie: Movie)
}
Each type of operation for a function is labeled with its own annotation: @Insert
, @Query
and @Update
, with the corresponding SQLite query included to modify the data in the database.
Next, you need to declare the Room database object itself. Open LocalDatabase.kt and examine how this is done:
@Database(entities = [Movie::class], version = 1)
@TypeConverters(IntegerListTypeConverter::class)
abstract class LocalDatabase : RoomDatabase() {...}
LocalDatabase
is abstract and inherits from RoomDatabase
. It’s marked with a Room @Database
annotation that lists the Entities that are in this database (corresponding to the tables in this database) and the version number.
The only entity in the WeWatch database is the Movie entity, so that’s the one that’s listed. The version number for the database is a number that you need to increment for database migrations if the schema of the database changes — but don’t worry about that right now.
The LocalDatabase.kt class then instantiates the Room database, like so:
LocalDatabase.INSTANCE =
Room.databaseBuilder(application, LocalDatabase::class.java, LocalDatabase.DB_NAME)
.allowMainThreadQueries()
.build()
A singleton pattern is used to create the Room database only once in the app.
The instantiation of the database occurs in LocalDataSource.kt. Open this file, and you’ll see that this is where the single instance of the database resides:
val db = LocalDatabase.getInstance(application)
LocalDataSource
is the source of data for the app, providing the list of movies from the database to the rest of the app. LocalDataSource
also acts as the point of contact for the rest of the app to expose the DAO’s other database calls. Any time a database call from the DAO is needed somewhere in the app, the calling Activity obtains an instance of LocalDataSource
and calls insert()
, delete()
and update()
.
LiveData
In some chapters of this book, you’ll use the RxJava library to perform asynchronous web calls and database operations on the Schedulers.io()
thread and to receive responses from them on the MainThread
. In the MVVM chapters, an alternative to RxJava is presented: LiveData.
LiveData is a lifecycle-aware component that wraps around objects you wish to emit in a reactive way, much the same way the Observable
object does in RxJava. LiveData is “live” in the sense that when the underlying data updates, anything observing that data will also receive the updates. For example, when paired with Room, LiveData retrieved from the Room database automatically updates when the data in the database changes.
In the MVVM with Android Architecture Components chapter project, the MovieDao.kt class returns a list of movies from the database as LiveData, like so:
@Query("select * from movie")
fun getAll(): LiveData<List<Movie>>
To learn more about LiveData, read chapter 11: MVVM with Android Architecture Components.
ViewModel
One problem with Activities and the Android lifecycle is that if the Activity gets destroyed, data held by that Activity also gets destroyed. So, how can you return the Activity to the state it was in once it is rebuilt? It’s possible to save some state through the Activity’s onSaveInstanceState(outState: Bundle?)
, but this solution is cumbersome and doesn’t work for more complicated data.
Android Architecture Components now offer a more robust way for data to survive configuration changes via the ViewModel. A ViewModel is a lifecycle-aware class used to hold onto the LiveData so that it doesn’t get destroyed on configuration changes like screen rotation. The ViewModel keeps its state throughout the Activity’s lifecycle, but it’s essential to avoid any references to Views or Activities within a ViewModel because these throw a nullPoinerException
when destroyed.
To see an example of how a ViewModel works, open MainViewModel.kt from the project for the MVVM with Android Architecture Components chapter.
class MainViewModel(private val repository: MovieRepository = MovieRepositoryImpl()): ViewModel() {}
Here, MainViewModel
extends ViewModel
and takes in a MovieRepository
in its constructor.
Next, check out how the ViewModel holds onto a list of movies as LiveData:
private val allMovies = MediatorLiveData<List<Movie>>()
init {
getAllMovies()
}
fun getSavedMovies() = allMovies
fun getAllMovies() {
allMovies.addSource(repository.getSavedMovies()) { movies ->
allMovies.postValue(movies)
}
}
The allMovies
list, in this case, is of type MediatorLiveData
, which is a subclass of regular LiveData
that allows you to mutate the LiveData and react to updates on the LiveData’s value via an onChanged()
event.
The app can then access allMovies
in any Activity, such as in MainActivity.kt, like so:
override fun onCreate(savedInstanceState: Bundle?) {
...
viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
viewModel.getSavedMovies().observe(this, Observer { movies ->
movies?.let {
adapter.setMovies(movies)
}
})
}
Here, the ViewModel is instantiated by using a ViewModelProvider
to provide the ViewModel. The Activity can then access the list of movies by calling viewModel.getSavedMovies()
and observing the LiveData returned from that call. In this example, the list of movies is set on the Adapter when observed.
Be sure to read Chapter 11: MVVM with Android Architecture Components to see the code in full.
Data binding
The Data Binding library allows you to bind UI components in your Layouts to data sources. Normally, this is done in code — such as setting the text of a TextView
using movieTitleTextView.setText = movie.title
— but with the Data Binding library, you’ll be able to do it directly in the XML Layout files.
For example, open the project from the chapter MVVM with Data Binding and look at this snippet of item_movie_main.xml:
<TextView
android:id="@+id/movieTitleTextView"
android:text="@={movie.title}"
... />
You can read more about data binding in chapter 10: MVVM with Data Binding chapter.
Key points
- Android Architecture Components are a set of libraries to help with various challenges in dealing with Android architecture.
- Room handles database persistence.
- Lifecycle helps you create components that are aware of the current Android lifecycle state.
- ViewModel holds data and survives configuration changes.
- LiveData provides observable data to views.
- Data binding allows Android Views to observe changes to data in XML.
- Paging handles displaying infinite lists.
- Navigation handles complex navigation.
- WorkManager manages background tasks.
Where to go from here?
Having a good understanding of the Android Architecture Components will be useful as you navigate the various chapters that use them. Next up, you’ll learn about another useful concept that’s present throughout the book: dependency injection.