Android Room Persistence With LiveData

Previously in our Room persistence article, we’ve discussed what are the basics of Room and how to simply work with it. Also, we’ve made a book library app using the Room persistence basic components. (If you haven’t read the previous article, I highly recommend you read it before proceeding it.)

In this article, we’re gonna see how we can use the Android Room Persistence library with LiveData. The Room library is built to work well with LiveData. The Room can return LiveData objects which are automatically notified when the database data changes and how their data is loaded in the Background Thread. This makes it easy to update the UI when database updates.

To start using the Room and LiveData together add the dependency in the build. gradle file.

// Room dependency
implementation 'android.arch.persistence.room:runtime:1.1.1'
annotationProcessor 'android.arch.persistence.room:compiler:1.1.1'

// LiveData dependency
implementation 'android.arch.lifecycle:viewmodel:1.1.1'
annotationProcessor 'android.arch.lifecycle:compiler:1.1.1'

To understand the concept of Room with LiveData, I’m going to use our previous article example. So, in this article, I’m not gonna tell you how to create Entity, Dao, and Database class for Room database. To see how to create basic components of Room see my previous article.

In our book library app, we define the BookDao class like below.

@Dao
interface BookDao : BaseDao<BookItem> {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insert(book: Book): Long

    @Query(value = "SELECT * FROM books")
    fun getAllBooks(): List<BookItem>

    @Query("SELECT * from books WHERE book_id = :bookId")
    fun getBookWithId(bookId : Int) : Book?

}

By using the above approach our app working is fine. We can store books in the database, we can retrieve all books, and we can successfully delete the books from the database.

Problem:

But we have a problem here let’s say, I want to read all the books from the database and after that whenever the changes happen in books column, I want to update my UI.

If I use the above solution then we’ll face these problems.

  1. Methods are synchronous in BookDao. So, we need to manually call the functions from the Background Thread. Because, if we call these methods from Main Thread, then the IllegalStateException will occur.
  2. Every time whenever the data changes in the database, again we need to call the getAllBooks method for updated books.
  3. After requesting the books from the database we have to listen to the result in the Main Thread.

Solution:

Like I said earlier, Room library is built to work with LiveData. So, the only thing we need to change is our BookDao methods return type. Now the BookDao class will look like this.


@Dao
interface BookDao : BaseDao<BookItem> {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insert(book: Book): Long

    @Query(value = "SELECT * FROM books")
    fun getAllBooks(): LiveData<List<BookItem>>

    @Query("SELECT * from books WHERE book_id = :bookId")
    fun getBookWithId(bookId : Int) : LiveData<Book>

}

Now our MainActivity will look like this.

class MainActivity : AppCompatActivity() {

    @Inject   // Getting instance from Dependency injection
    lateinit var BookDao bookDao
    private val allBooks : MutableList<BookItem> = ListView()

    override fun onCreate(savedInstanceState: Bundle?) {
        .......
        .......
        bookDao.allBooks()
                .observe(this, object : Observer<List<BookItem>> {
                    override fun onChanged(books: List<BookItem>?) {
                        if(allBooks.isNotEmpty()) 
                           allBooks.clear
                        this.allBooks.addAll(books)
                        showDataInListView(allBooks)
                    }
                })
    }
}

Please pay attention here, first by using the LiveData we don’t need to call database methods from the Background Thread and listen to the result in Main Thread because LiveData does all of this for us under the hood. Second, whenever the changes happen in the database, we don’t need to call every time for an updated result because the onChanged method will be triggered automatically with the updated value. And in third, the LiveData Observer is bound to this Activity lifecycle. It automatically removes the emission if the activity is destroyed.

Another Example

So let’s say if I want to get the BookItem by Id that doesn’t exist in the database. So in that case, LiveData simply return null. To solve this null-ability when the data is missing you can either work with Optional or you can work with Flowable to return List of data. By adding the Optional our BookDao class will look like this.

@Dao
abstract class BookDao {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    abstract fun insert(book: BookItem): Long

    @Query(value = "SELECT * FROM books")
    abstract fun getAllBooks(): List<BookItem>

    @Query("SELECT * from books WHERE book_id = :bookId")
    abstract fun getBookWithId(bookId: Int): LiveData<Optional<BookItem>>
}

Now, If I’m trying to read the non-existing BookItem from the database it will return the Optional. empty instead of null. If you want to work with Flowable or other RxJava observers you can check out my article on RxJava2 with Room.

Protip:

Now instead of directly observing the data from the database. We can implement a ViewModel class to fetch the data from the database and after that our UI only needs to observe that LiveData.

Below is the BookViewModel class.

class BookViewModel : ViewModel() {

    @Inject
    lateinit var bookDao : BookDao 

    val bookItems: LiveData<List<BookItem>> = bookDao.allBooks()
}

And now the MainActivity will look like this.

class LiveDataSwitchMapExample : AppCompatActivity() {

    private val bookItems: MutableList<BookItem> = ArrayList()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.live_data_swicth_map_example)
        val bookViewModel = ViewModelProviders.of(this).get(BookViewModel::class.java)
        bookViewModel.bookItems.observe(this, object : Observer<List<BookItem>> {

            override fun onChanged(t: List<BookItem>?) {
                if (bookItems.isNotEmpty())
                    bookItems.clear()
                bookItems.addAll(it!!)
            }
        })
    }
}

The MainActivity is pretty much the same but by adding the ViewModel class, but we add the transparency to our code. By transparency I mean, our UI doesn’t need to know from where the BookItems are coming from. It may be coming from Network source, it may be coming from the local database or may be coming from the Cache. You get my point, UI only needs to worry about how to show data to the user not how to fetch.

To read more about ViewModel you can check out this article on ViewModel

Another Example:

Sometime there may be a case where you need to modify the data and then propagates the result to the downstream. Let’s say we only want bookName instead of a complete BookItem object. Here comes the concept of LiveData Transformations. Below shows how we can use the LiveData Transformations in our ViewModel class.

class BookViewModel : ViewModel() {

    @Inject
    private lateinit var bookDao: BookDao

    val bookItems: LiveData<List<String>> = Transformations.map(bookDao.getAllBooks(), ::getBookName)

    private fun getBookName(books: List<BookItem>): List<String> {
        val booksName: MutableList<String> = ArrayList()
        books.forEach { booksName.add(it.bookName) }
        return booksName
    }
}

Now we’re only emitting the bookNames instead of the complete book object. For to further reading about LiveData Transformation see this article. In that article, I briefly explain how the map and switchMap transformation works.

Below is the MainActivity class.

class LiveDataSwitchMapExample : AppCompatActivity() {

    private val bookNames: MutableList<String> = ArrayList()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.live_data_swicth_map_example)
        val bookViewModel = ViewModelProviders.of(this).get(BookViewModel::class.java)
        bookViewModel.bookItems.observe(this, object : Observer<List<String>> {
            override fun onChanged(t: List<String>?) {
                if (bookNames.isNotEmpty())
                    bookNames.clear()
                bookNames.addAll(t)
                // showBookNames(bookNames)
            }
        })
    }
}

You see in our MainActivity, I’m expecting bookNames instead of complete book objects.

Alright, guys, this is my simple attempt to fetching data from the database with Room Persistence and LiveData. You can get the complete source code of the above app from GitHub. If you’ve any queries regarding this post or any question about Room Persistence please do comment below.

Thank you for being here and keep reading…

LEAVE A REPLY

Please enter your comment!
Please enter your name here