Providing offline capabilities to your Android app

Liviu Iuga
Zipper Studios
Published in
5 min readMay 24, 2019

Ever wondered how your favorite Android app remembers all its content even when the internet connection is offline? Wonder no more, Room is here to save the day.

What is Room you ask? Simply put, it’s an abstraction layer over SQLite. Which means you get all the benefits of an SQLite database without the headaches. Using annotations, Room is able to identify your Java/Kotlin classes and use them to provide a fully functional database.

Implementing such a feature is no rocket science, we begin by adding the required dependencies:

implementation 'android.arch.persistence.room:rxjava2:1.1.1'
implementation 'android.arch.persistence.room:runtime:1.1.1'
kapt 'android.arch.persistence.room:compiler:1.1.1'

To fully explore the power of Room, I’ve decided to build a simple ToDoList app. The code snippets we’re going to take a look at are taken from it. For full project access, scroll down to the bottom of the page and click on the Github link. Moving on, we should get familiar with the 3 main components that Room makes use of:

Entity: Which is a table within the database. Basically, this will be your model class, whatever it is that you want to store. Since the item that we’re going to use represents a task, we’re going to give it 2 fields (title and priority).

@Entity(tableName = "itemToDo")
class ItemToDo(
@PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "title") val title: String,
@ColumnInfo(name = "priority") val priority: String
) {
@Ignore
constructor() : this("", "")
}

Note: Room does not allow multiple constructors in an Entity class, but in this project, Firebase is used and to properly map the response we need an empty constructor. Solution? Use the @Ignore annotation to tell Room to ignore the second constructor.

DAO: Contains the methods used for accessing the database. This is an interface that acts as a mediator between the database and your content handler( in this case, ViewModel). Although Room provides methods for updating and deleting items, for now, we’re just going to add items and display them.

@Dao
interface ToDoDao {
@Query("SELECT * from itemToDo")
fun getAllToDoItems(): Single<List<ItemToDo>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAllItems(list: MutableList<ItemToDo>)
}

Database: contains the database holder and has to satisfy 3 conditions:

  • It has to be an abstract class that extends RoomDatabase
  • It must include the list of entities that are going to be used
  • It has to contain an abstract method with no arguments that return the class annotated with @Dao
@Database(entities = [ItemToDo::class], version = 1)
abstract class ToDoDatabase : RoomDatabase() {
abstract fun toDoDao(): ToDoDao
}

Now that we have those required components, let’s put them to good use. We’re going to need 2 activities, MainActivity for displaying the items and one for inserting new items, AddItemActivity.

Hopefully, you already are familiar with the MVVM pattern. If not, there are plenty of tutorials out there that will put you up to date, but just to give you a really quick idea what this weird letter abbreviation is all about, with this pattern, all the business logic resides inside the ViewModel while the activity is responsible only for displaying the results.

The code for AddItemActivity is pretty straightforward. We have an EditText, a RadioGroup with 3 possible selections to allow the user to set the priority and an “AddItem” Button that will execute the addItem function. Inside the ViewModel resides the following:

class AddItemViewModel(val repository: Repository) : ViewModel() {

private var priority = "Normal"
private lateinit var toDoText: String
val itemAdded = MutableLiveData<Boolean>()

var textWatcher = object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
// Do nothing.
}

override fun beforeTextChanged(s: CharSequence?, start: Int,
count: Int, after: Int) {
// Do nothing.
}

override fun onTextChanged(s: CharSequence?, start: Int,
before: Int, count: Int) {
toDoText = s.toString()
}

}

fun addItem() {
val itemToDo = ItemToDo(toDoText, priority)
repository.addToDoItem(itemToDo)
itemAdded.value = true
}

fun onCheckedChanged(priority: String) {
this.priority = priority
}
}
override fun beforeTextChanged(s: CharSequence?, start: Int,
count: Int, after: Int) {
// Do nothing.
}
override fun onTextChanged(s: CharSequence?, start: Int,
before: Int, count: Int) {
toDoText = s.toString()
}
} fun addItem() {
val itemToDo = ItemToDo(toDoText, priority)
repository.addToDoItem(itemToDo)
itemAdded.value = true
}
fun onCheckedChanged(priority: String) {
this.priority = priority
}
}

How did I get the text the user entered? I created a TextWatcher and using BindingAdapters I bound it to the EditText. This will listen for text changes, assigning it to our toDoText String variable. To each of the 3 RadioButtons, I assigned the onCheckedChanged method to the onClick attribute. All that’s left is to use the values to create an ItemToDo object and add it to the firebase database.

Displaying the items

Here’s where Room begins to shine and make itself useful. In the Repository class injected in both of our ViewModels, we define 3 functions: one for retrieving items from firebase (or a database of your choice), one for inserting the items we just retrieved into the local database (Room), and one for displaying the items we’ve just inserted.

private var comp: CompositeDisposable = CompositeDisposable()

fun getItemsFromFirebase() {
val firebaseReference = FirebaseDatabase.getInstance().reference

firebaseReference.addListenerForSingleValueEvent(object :
ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
addFirebaseResponseToLocalDB(dataSnapshot)
}

override fun onCancelled(databaseError: DatabaseError) {
return
}
})
}

fun addFirebaseResponseToLocalDB(dataSnapshot: DataSnapshot) {
val toDoItemList = ArrayList<ItemToDo>()
val memberSnapshot = dataSnapshot.children
for (child in memberSnapshot) {
val itemToDo = child.getValue(ItemToDo::class.java)
itemToDo?.let { toDoItemList.add(itemToDo) }
}
insertItemsInLocalDB(toDoItemList)
}

fun getItemsFromLocalDatabase(): LiveData<List<ItemToDo>> {
return db.toDoDao().getAllToDoItems()
}

fun insertItemsInLocalDB(toDoItemList: ArrayList<ItemToDo>) {
comp.add(Observable.fromCallable
{db.toDoDao().insertItems(toDoItemList) }
.subscribeOn(Schedulers.io())
.subscribe())
}

In MainActivity’s ViewModel we define a variable named itemsList. This will be a list of items that we will retrieve from our local DB. If you paid close attention to the snippet of code above, you noticed the function that retrieves these items extends LiveData. This will be very helpful along the way, making things easier for us.

The onResume function is called from the Activity, retrieving items from Firebase each time MainActivity comes in the foreground.

class MainViewModel(val repository: Repository) : ViewModel() {

val mainAdapter = MainAdapter()
val itemsList = repository.getItemsFromLocalDatabase()

fun updateList(it: List<ItemToDo>) {
mainAdapter.updateList(it)
}

fun onResume() {
repository.getItemsFromFirebase()
}
}

And here comes the beautiful part of combining LiveData with Room. The only thing we need to do is to observe the itemsList in our ViewModel. Whenever items are inserted in Room, our itemsList is updated (kudos to LiveData), this event is observed in the activity, and the updateList is called to properly handle these changes in the adapter.

override fun onCreate(savedInstanceState: Bundle?) {
.....
viewModel.itemsList.observe(this, Observer { list ->
list?.let {
viewModel.updateList(it)
}
}
)
}

fun startAddItemActivity(v: View) {
val intent = Intent(this, AddItemActivity::class.java)
startActivity(intent)
}

override fun onResume() {
super.onResume()
viewModel.onResume()
}

That’s it? Yeah, that’s it! Cool, huh? Take a closer look at Room in action by checking out the code from this ToDoList sample app. Don’t forget to clap if this article helped you.

Zipper Studios is a group of passionate engineers helping startups and well-established companies build their mobile products. Our clients are leaders in the fields of health and fitness, AI, and Machine Learning. We love to talk to likeminded people who want to innovate in the world of mobile so drop us a line here.

--

--