The purpose of Architecture Components is to provide guidance on app architecture, with libraries for common tasks like lifecycle management and data persistence.
Architecture components help you structure your app in a way that is robust, testable, and maintainable with less boilerplate code.
This is the Kotlin version of the codelab. The Java version here.
Here is a short introduction to the Architecture Components and how they work together. Note that this codelab focuses on a subset of the components, namely LiveData, ViewModel and Room. Each component is explained more as you use it.
This diagram shows a basic form of this architecture:
Entity: When working with Room, this is an annotated class that describes a database table.
SQLite database: On the device, data is stored in an SQLite database. The Room persistence library creates and maintains this database for you.
DAO: Data access object. A mapping of SQL queries to functions. When you use a DAO, you call the methods, and Room takes care of the rest.
Room database: Database layer on top of SQLite database that takes care of mundane tasks that you used to handle with an SQLiteOpenHelper
. Database holder that serves as an access point to the underlying SQLite database. The Room database uses the DAO to issue queries to the SQLite database.
Repository: A class that you create. You use the Repository for managing multiple data sources.
ViewModel: Provides data to the UI. Acts as a communication center between the Repository and the UI. Hides where the data originates from the UI. ViewModel instances survive Activity/Fragment recreation.
LiveData: A data holder class that can be observed. Always holds/caches latest version of data. Notifies its observers when the data has changed. LiveData
is lifecycle aware. UI components just observe relevant data and don't stop or resume observation. LiveData automatically manages all of this since it's aware of the relevant lifecycle status changes while observing.
You will build an app that uses Android Architecture Components and implements the architecture from Guide to App Architecture for these components. The sample app stores a list of words in a Room database and displays it in a RecyclerView
. The app is bare bones but sufficiently complex that you can use it as a template to build upon.
In this codelab you build an app that does the following:
RecyclerView
in MainActivity
.The following diagram shows all the pieces of the app. Each of the enclosing boxes (except for the SQLite database) represents a class that you will create.
There are a lot of steps to using the Architecture Components and implementing the recommended architecture. The most important thing is to create a mental model of what is going on, and understand how the pieces fit together and how the data flows. As you work through this codelab, don't just copy and paste the code, but try to start building that inner understanding.
You need to be familiar with Kotlin, object-oriented design concepts, and Android Development Fundamentals. In particular:
RecyclerView
and adaptersIf you are not familiar with Kotlin, a version of this codelab is provided in Java here.
This codelab is focused on Android Architecture Components. Off-topic concepts and code are provided for you to simply copy and paste.
This codelab provides all the code you need to build the complete app.
Open Android Studio and create an app as follows:
You have to add the component libraries to your gradle files.
In your build.gradle
(Module: app) make the following changes:
Apply the kapt Kotlin plugin by adding it after the other plugins defined on the top of your build.gradle
(Module: app) file.
apply plugin: 'kotlin-kapt'
Add the following code to the dependencies
block.
// Room components
implementation "android.arch.persistence.room:runtime:$rootProject.roomVersion"
kapt "android.arch.persistence.room:compiler:$rootProject.roomVersion"
androidTestImplementation "android.arch.persistence.room:testing:$rootProject.roomVersion"
// Lifecycle components
implementation "android.arch.lifecycle:extensions:$rootProject.archLifecycleVersion"
kapt "android.arch.lifecycle:compiler:$rootProject.archLifecycleVersion"
// Coroutines
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$rootProject.coroutines"
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$rootProject.coroutines"
Enable the coroutines support by adding the following code at the end of your build.gradle (Module: app) file.
kotlin {
experimental {
coroutines "enable"
}
}
In your build.gradle
(Project: RoomWordsSample) file, add the version numbers to the end of the file, as given in the code below.
ext {
roomVersion = '1.1.1'
archLifecycleVersion = '1.1.1'
coroutines = '0.26.1'
}
The data for this app is words, and each word is an Entity. Create a data class called Word
that describes a word Entity. You need public values for the columns, because that's how Room
knows to instantiate your objects.
Here is the code:
data class Word(val word: String)
To make the Word
class meaningful to a Room database, you need to annotate it. Annotations identify how each part of this class relates to an entry in the database. Room uses this information to generate code.
@Entity(tableName =
"word_table"
)
@Entity
class represents an entity in a table. Annotate your class declaration to indicate that it's an entity. Specify the name of the table if you want it to be different from the name of the class.@PrimaryKey
@ColumnInfo(name =
"word"
)
You can find a complete list of annotations in the Room package summary reference.
Update your Word
class with annotations as shown in this code. If you type the annotations, Android Studio will auto-import.
@Entity(tableName = "word_table")
class Word(@PrimaryKey @ColumnInfo(name = "word") val word: String)
In the DAO (data access object), you specify SQL queries and associate them with method calls. The compiler checks the SQL and generates queries from convenience annotations for common queries, such as @Insert
.
The DAO must be an interface or abstract class.
By default, all queries must be executed on a separate thread.
Room uses the DAO to create a clean API for your code.
The DAO for this codelab is basic and provides queries for getting all the words, inserting a word, and deleting all the words.
WordDao
. @Dao
to identify it as a DAO class for Room. fun insert(word: Word)
@Insert
. You don't have to provide any SQL! (There are also @Delete
and @Update
annotations for deleting and updating a row, but you are not using them in this app.)fun deleteAll()
.@Query
.@Query("DELETE FROM word_table")
List
of Words
.fun getAllWords(): List<Word>
@Query(
"SELECT * from word_table ORDER BY word ASC"
)
Here is the completed code:
@Dao
interface WordDao {
@Query("SELECT * from word_table ORDER BY word ASC")
fun getAllWords(): List<Word>
@Insert
fun insert(word: Word)
@Query("DELETE FROM word_table")
fun deleteAll()
}
When data changes you usually want to take some action, such as displaying the updated data in the UI. This means you have to observe the data so that when it changes, you can react. Depending on how the data is stored, this can be tricky. Observing changes to data across multiple components of your app can create explicit, rigid dependency paths between the components. This makes testing and debugging difficult, among other things.
LiveData
, a lifecycle library class for data observation, solves this problem. Use a return value of type LiveData
in your method description, and Room generates all necessary code to update the LiveData
when the database is updated.
In WordDao
, change the getAllWords()
method signature so that the returned List<Word>
is wrapped with LiveData
.
@Query("SELECT * from word_table ORDER BY word ASC")
fun getAllWords(): LiveData<List<Word>>
Later in this codelab, you create an Observer
of the data in the onCreate()
method of MainActivity.
Overriding the observer's onChanged()
method makes it possible to receive notifications when the LiveData
changes, in the onChanged()
method. You will then update the cached data in the adapter, and the adapter will update what the user sees.
SQLiteOpenHelper
. LiveData
, the queries are automatically runasynchronously on a background thread.Your Room database class must be abstract and extend RoomDatabase
. Usually, you only need one instance of a Room database for the whole app.
public abstract
class that extends RoomDatabase
and call it WordRoomDatabase
. public abstract class
WordRoomDatabase
:
RoomDatabase() {}
@Database(entities = {Word.
class
}, version = 1)
abstract
fun wordDao(): WordDao
Here is the code:
@Database(entities = [Word::class], version = 1)
public abstract class WordRoomDatabase : RoomDatabase() {
abstract fun wordDao(): WordDao
}
WordRoomDatabase
a singleton to prevent having multiple instances of the database opened at the same time. Here is the code:
companion object {
@Volatile
private var INSTANCE: WordRoomDatabase? = null
fun getDatabase(context: Context): WordRoomDatabase {
return INSTANCE ?: synchronized(this) {
// Create database here
val instance = // TODO
INSTANCE = instance
instance
}
}
}
RoomDatabase
object in the application context from the WordRoomDatabase
class and names it "word_database"
. // Create database here
val instance = Room.databaseBuilder(
context.applicationContext,
WordRoomDatabase::class.java,
"Word_database"
).build()
Here is the complete code for the class:
@Database(entities = arrayOf(Word::class), version = 1)
public abstract class WordRoomDatabase : RoomDatabase() {
abstract fun wordDao(): WordDao
companion object {
@Volatile
private var INSTANCE: WordRoomDatabase? = null
fun getDatabase(context: Context): WordRoomDatabase {
val tempInstance = INSTANCE
if (tempInstance != null) {
return tempInstance
}
synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
WordRoomDatabase::class.java,
"Word_database"
).build()
INSTANCE = instance
return instance
}
}
}
}
A Repository
class abstracts access to multiple data sources. The Repository is not part of the Architecture Components libraries, but is a suggested best practice for code separation and architecture. A Repository
class provides a clean API for data access to the rest of the application.
A Repository manages queries and allows you to use multiple backends. In the most common example, the Repository implements the logic for deciding whether to fetch data from a network or use results cached in a local database.
WordRepository
. LiveData
will notify the observer when the data has changed.class WordRepository(private val wordDao: WordDao) {
}
LiveData
will notify the observer when the data has changed.val allWords: LiveData<List<Word>> = wordDao.getAllWords()
insert()
method. You must call this on a non-UI thread or your app will crash. Room ensures that you don't do any long-running operations on the main thread, blocking the UI. Add the @WorkerThread
annotation, to mark that this method needs to be called from a non-UI thread. Add the suspend
modifier to tell the compiler that this needs to be called from a coroutine or another suspending function. @WorkerThread
suspend fun insert(word: Word) {
wordDao.insert(word)
}
Here is the complete code:
class WordRepository(private val wordDao: WordDao) {
val allWords: LiveData<List<Word>> = wordDao.getAllWords()
@WorkerThread
suspend fun insert(word: Word) {
wordDao.insert(word)
}
}
The ViewModel
's role is to provide data to the UI and survive configuration changes. A ViewModel
acts as a communication center between the Repository and the UI. You can also use a ViewModel
to share data between fragments. The ViewModel is part of the lifecycle library.
For an introductory guide to this topic, see ViewModel
.
A ViewModel
holds your app's UI data in a lifecycle-conscious way that survives configuration changes. Separating your app's UI data from your Activity
and Fragment
classes lets you better follow the single responsibility principle: Your activities and fragments are responsible for drawing data to the screen, while your ViewModel
can take care of holding and processing all the data needed for the UI.
In the ViewModel
, use LiveData
for changeable data that the UI will use or display. Using LiveData
has several benefits:
ViewModel
. There are no database calls from the ViewModel
, making the code more testable.WordViewModel
that gets the Application
as a parameter and extends AndroidViewModel
. class WordViewModel(application: Application) : AndroidViewModel(application)
private val repository: WordRepository
LiveData
member variable to cache the list of words. val allWords: LiveData<List<Word>>
init
block that gets a reference to the WordDao
from the WordRoomDatabase
and constructs the WordRepository
based on it. init {
val wordsDao = WordRoomDatabase.getDatabase(application).wordDao()
repository = WordRepository(wordsDao)
}
init
block, initialize the allWords
property with the data from repository:allWords = repository.allWords
parentJob
, and a coroutineContext.
The coroutineContext, by default, uses the parentJob
and the main dispatcher to create a new instance of a CoroutineScope
based on the coroutineContext
.private var parentJob = Job()
private val coroutineContext: CoroutineContext
get() = parentJob + Dispatchers.Main
private val scope = CoroutineScope(coroutineContext)
onCleared
method and cancel the parentJob
. onCleared
is called when the ViewModel
is no longer used and will be destroyed so, now is the time to cancel any long running jobs done by the parentJob
. override fun onCleared() {
super.onCleared()
parentJob.cancel()
}
insert()
method that calls the Repository's insert()
method. In this way, the implementation of insert()
is completely hidden from the UI. We want the insert()
method to be called away from the main thread, so we're launching a new coroutine, based on the coroutine scope defined previously. Because we're doing a database operation, we're using the IO Dispatcher
.fun insert(word: Word) = scope.launch(Dispatchers.IO) {
repository.insert(word)
}
Here is the complete code for WordViewModel
:
class WordViewModel(application: Application) : AndroidViewModel(application) {
private var parentJob = Job()
private val coroutineContext: CoroutineContext
get() = parentJob + Dispatchers.Main
private val scope = CoroutineScope(coroutineContext)
private val repository: WordRepository
val allWords: LiveData<List<Word>>
init {
val wordsDao = WordRoomDatabase.getDatabase(application).wordDao()
repository = WordRepository(wordsDao)
allWords = repository.allWords
}
fun insert(word: Word) = scope.launch(Dispatchers.IO) {
repository.insert(word)
}
override fun onCleared() {
super.onCleared()
parentJob.cancel()
}
}
Next, you need to add the XML layout for the list and items.
This codelab assumes that you are familiar with creating layouts in XML, so we are just providing you with the code.
Add a style for list items in values/styles.xml
:
<!-- The default font for RecyclerView items is too small.
The margin is a simple delimiter between the words. -->
<style name="word_title">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">26dp</item>
<item name="android:textSize">24sp</item>
<item name="android:textStyle">bold</item>
<item name="android:layout_marginBottom">6dp</item>
<item name="android:paddingLeft">8dp</item>
</style>
Add a layout/recyclerview_item.xml
layout:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/textView"
style="@style/word_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/holo_orange_light" />
</LinearLayout>
In layout/content_main.xml
, replace the TextView
with a RecyclerView
:
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/darker_gray"
tools:listitem="@layout/recyclerview_item" />
Your floating action button (FAB) should correspond to the available action. In the layout/activity_main.xml
file, give the FloatingActionButton
a +
symbol icon:
layout/activity_main.xml
file, select File > New > Vector Asset.+
("add") asset. android:src="@drawable/ic_add_black_24dp"
You are going to display the data in a RecyclerView
, which is a little nicer than just throwing the data in a TextView
. This codelab assumes that you know how RecyclerView
, RecyclerView.LayoutManager
, RecyclerView.ViewHolder
, and RecyclerView.Adapter
work.
Note that the words
variable in the adapter caches the data. In the next task, you add the code that updates the data automatically.
Add a class WordListAdapter
that extends RecyclerView.Adapter
. Here is the code.
class WordListAdapter internal constructor(
context: Context
) : RecyclerView.Adapter<WordListAdapter.WordViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var words = emptyList<Word>() // Cached copy of words
inner class WordViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val wordItemView: TextView = itemView.findViewById(R.id.textView)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WordViewHolder {
val itemView = inflater.inflate(R.layout.recyclerview_item, parent, false)
return WordViewHolder(itemView)
}
override fun onBindViewHolder(holder: WordViewHolder, position: Int) {
val current = words[position]
holder.wordItemView.text = current.word
}
internal fun setWords(words: List<Word>) {
this.words = words
notifyDataSetChanged()
}
override fun getItemCount() = words.size
}
Add the RecyclerView
in the onCreate()
method of MainActivity
.
In the onCreate()
method:
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
val adapter = WordListAdapter(this)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
Run your app to make sure everything works. There are no items, because you have not hooked up the data yet, so the app should display a gray background without any list items.
There is no data in the database. You will add data in two ways: Add some data when the database is opened, and add an Activity
for adding words.
To delete all content and repopulate the database whenever the app is started, you create a RoomDatabase.Callback
and override onOpen()
. Because you cannot do Room database operations on the UI thread, onOpen()
launches a coroutine on the IO Dispatcher.
To launch a coroutine we need a CoroutineScope
. Update the getDatabase
method of the RoomDatabase
class, to also get a coroutine scope as parameter:
fun getDatabase(
context: Context,
scope: CoroutineScope
): WordRoomDatabase {
...
}
Update the database retrieval in the init
block of WordViewModel
, to also pass the scope
val wordsDao = WordRoomDatabase.getDatabase(application, scope).wordDao()
In the WordRoomDatabase
, we create a custom implementation of the RoomDatabase.Callback()
, that also gets a CoroutineScope
as constructor parameter. Then, we override the onOpen
method to populate the database.
Here is the code for creating the callback in the WordRoomDatabase
class:
private class WordDatabaseCallback(
private val scope: CoroutineScope
) : RoomDatabase.Callback() {
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
INSTANCE?.let { database ->
scope.launch(Dispatchers.IO) {
populateDatabase(database.wordDao())
}
}
}
}
Here is the code for the function that deletes the contents of the database, then populates it with the two words "Hello" and "World". Feel free to add more words!
fun populateDatabase(wordDao: WordDao) {
wordDao.deleteAll()
var word = Word("Hello")
wordDao.insert(word)
word = Word("World!")
wordDao.insert(word)
}
Finally, add the callback to the database build sequence right before calling .build()
.
.addCallback(WordDatabaseCallback(scope))
Add these string resources in values/strings.xml
:
<string name="hint_word">Word...</string>
<string name="button_save">Save</string>
<string name="empty_not_saved">Word not saved because it is empty.</string>
Add this color resource in value/colors.xml
:
<color name="buttonLabel">#d3d3d3</color>
Add these dimension resources in values/dimens.xml
:
<dimen name="small_padding">6dp</dimen>
<dimen name="big_padding">16dp</dimen>
Use the Empty Activity template to create a new activity, NewWordActivity
. Verify that the activity has been added to the Android Manifest!<
activity android:name=".NewWordActivity"
></
activity
>
Here is the code for the activity:
class NewWordActivity : AppCompatActivity() {
private lateinit var editWordView: EditText
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_new_word)
editWordView = findViewById(R.id.edit_word)
val button = findViewById<Button>(R.id.button_save)
button.setOnClickListener {
val replyIntent = Intent()
if (TextUtils.isEmpty(editWordView.text)) {
setResult(Activity.RESULT_CANCELED, replyIntent)
} else {
val word = editWordView.text.toString()
replyIntent.putExtra(EXTRA_REPLY, word)
setResult(Activity.RESULT_OK, replyIntent)
}
finish()
}
}
companion object {
const val EXTRA_REPLY = "com.example.android.wordlistsql.REPLY"
}
}
Update the activity_new_word.xml
file in the layout folder, with the following code:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/edit_word"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-light"
android:hint="@string/hint_word"
android:inputType="textAutoComplete"
android:padding="@dimen/small_padding"
android:layout_marginBottom="@dimen/big_padding"
android:layout_marginTop="@dimen/big_padding"
android:textSize="18sp" />
<Button
android:id="@+id/button_save"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
android:text="@string/button_save"
android:textColor="@color/buttonLabel" />
</LinearLayout>
The final step is to connect the UI to the database by saving new words the user enters and displaying the current contents of the word database in the RecyclerView
.
To display the current contents of the database, add an observer that observes the LiveData
in the ViewModel
. Whenever the data changes, the onChanged()
callback is invoked, which calls the adapter's setWord()
method to update the adapter's cached data and refresh the displayed list.
In MainActivity
, create a member variable for the ViewModel
:
private lateinit var wordViewModel: WordViewModel
Use ViewModelProviders
to associate your ViewModel
with your Activity
. When your Activity
first starts, the ViewModelProviders
will create the ViewModel
. When the activity is destroyed, for example through a configuration change, the ViewModel
persists. When the activity is re-created, the ViewModelProviders
return the existing ViewModel
. For more information see ViewModel
.
In onCreate()
, get a ViewModel
from the ViewModelProvider
.
wordViewModel = ViewModelProviders.of(this).get(WordViewModel::class.java)
Also in onCreate()
, add an observer for the LiveData
returned by getAllWords()
.
The onChanged()
method fires when the observed data changes and the activity is in the foreground.
wordViewModel.allWords.observe(this, Observer { words ->
// Update the cached copy of the words in the adapter.
words?.let { adapter.setWords(it) }
})
Define a request code as a member of the MainActivity
:
companion object {
const val newWordActivityRequestCode = 1
}
In MainActivity
, add the onActivityResult()
code for the NewWordActivity
.
If the activity returns with RESULT_OK
, insert the returned word into the database by calling the insert()
method of the WordViewModel
.
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == newWordActivityRequestCode && resultCode == Activity.RESULT_OK) {
data?.let {
val word = Word(it.getStringExtra(NewWordActivity.EXTRA_REPLY))
wordViewModel.insert(word)
}
} else {
Toast.makeText(
applicationContext,
R.string.empty_not_saved,
Toast.LENGTH_LONG).show()
}
}
In MainActivity,
start NewWordActivity
when the user taps the FAB. Replace the code in the FAB's onClick()
click handler with this code:
fab.setOnClickListener {
val intent = Intent(this@MainActivity, NewWordActivity::class.java)
startActivityForResult(intent, newWordActivityRequestCode)
}
RUN YOUR APP!!!
When you add a word to the database in NewWordActivity
, the UI will automatically update.
Now that you have a working app, let's recap what you've built. Here is the app structure again:
The components of the app are:
MainActivity
: displays words in a list using a RecyclerView
and the WordListAdapter
. In the MainActivity
, there is an Observer
that observes the words LiveData from the database and is notified when they change.NewWordActivity:
adds a new word to the list.WordViewModel
(*): provides methods for accessing the data layer, and it returns LiveData so that MainActivity can set up the observer relationship.LiveData<List<Word>>
: Makes possible the automatic updates in the UI components. In the MainActivity
, there is an Observer
that observes the words LiveData from the database and is notified when they change.Repository:
manages one or more data sources. The Repository
exposes methods for the ViewModel to interact with the underlying data provider. In this app, that backend is a Room database. Room
: is a wrapper around and implements a SQLite database. Room does a lot of work for you that you used to have to do yourself. getAllWords()
, Room can execute SELECT * from word_table ORDER BY word ASC
.Word
: is the entity class that contains a single work.(*)Views
and Activities
(and Fragments
) only interact with the data through the ViewModel
. As such, it doesn't matter where the data comes from.
Click the following link to download the solution code for this codelab:
Unpack the downloaded zip file. This will unpack a root folder, android-room-with-a-view-kotlin
, which contains the complete app.