Room With LiveData
| | | | | |

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. If you’ve any queries regarding the above post or any question about Room Persistence please do comment below.

Thank you for being here and keep reading…

Similar Posts

6 Comments

  1. Thank you Ahsen for responding! I figured out that it was my query after a few log outputs. This one thing cost me hours… 🙁 I still appreciate an responding expert!

    @Query(“SELECT message_table.message,message_table.message_title,message_table._id,message_table.employee_id,message_table.message_status_id,” +
    “message_table.alert_date,message_table.close_date,message_table.add_date FROM message_table ” +
    “INNER JOIN message_status_table ON message_table.message_status_id = message_status_table._id ” +
    “LEFT JOIN employee_table e1 ON message_table.employee_id = e1._id WHERE message_table.close_date > date(‘now’) or message_table.close_date is null order by alert_date desc”)
    List getAllMessagesTest();

    ** I do have a question on LiveData.

    I have this query in my DAO … @Query(“SELECT customer_table.first_name,customer_table.last_name, ” +
    “customer_table.address, state_table.state_abr, customer_table.city, customer_table.zip, ” +
    “customer_table.email, customer_table.phone, customer_table.active, customer_table.add_date, ” +
    “customer_table.modify_date, status_table.status_name, customer_table.customer_type, customer_table.note, ” +
    “referral_table.referral_name,e1.first_name + ‘ ‘ + e1.last_name as technician, ” +
    “customer_table.commercial_name,e2.first_name + ‘ ‘ + e2.last_name as assignedBy, ” +
    “customer_table.domestic_animal, customer_table.visit_date, customer_table.initial_sale, customer_table.cancellation_reason ” +
    “FROM customer_table ” +
    “INNER JOIN status_table ON customer_table.status_id = status_table._id ” +
    “LEFT JOIN state_table ON customer_table.state_id = state_table._id ” +
    “LEFT JOIN referral_table ON customer_table.referral_id = referral_table._id ” +
    “LEFT JOIN employee_table e1 ON customer_table.technician_id = e1._id ” +
    “LEFT JOIN employee_table e2 ON customer_table.technician_id = e2._id ” +
    “WHERE customer_table._id=:id”)
    LiveData<List> findCustomer(final Integer id);

    static class Customer {
    private String first_name;
    private String last_name;
    private String email;
    private String address;
    private String city;
    private String state_abr;
    private String zip;
    private String phone;
    private boolean active;
    private String add_date;
    private String modify_date;
    private String status_name;
    private boolean customer_type;
    private String note;
    private String technician;
    private String assignedBy;
    private String commercial_name;
    private String domestic_animal;
    private String referral_name;
    private String visit_date;
    private BigDecimal initial_sale;
    private String cancellation_reason;
    }

    According to Android site, I need to reference in View Model which connects to the Repository, with MutableLiveData Transformation. I want to get a LiveData List Object based on an integer input and return that object. I know something below is off but not quite understanding. Can you help? I’m trying to be as efficient as I can but my under standing is a bit foggy. What should be changed and why? I know the respository function has got to wrong. 🙂

    public class InspectionChecklistViewModel extends AndroidViewModel {
    private InspectionChecklistRepository repository;
    private LiveData<List> allInspectionChecklists;

    MutableLiveData userIDLiveDate ;
    LiveData userLiveDate = Transformations.switchMap(userIDLiveData, id -> repository.findCustomerInspectionChecklist(id));

    public InspectionChecklistViewModel(@NonNull Application application) {
    super(application);
    repository = new InspectionChecklistRepository(application);
    allInspectionChecklists = repository.getAllInspectionChecklists();

    }

    But in Repository …. I have the following function

    public List findCustomerInspectionChecklist(Integer id) throws ExecutionException, InterruptedException {
    return new InspectionChecklistRepository.findCustomerInspectionChecklistAsyncTask().execute(id).get();
    }

    1. Hey Anne,
      I’m happy that your problem solved. I’ve been looking at your code and I don’t understand why do you need to return the LiveData>. The LiveData is only needed when you need to listen to new changes. Also, the switchMap is needed of you need to apply some kind of transformation to your live data object

      Still if you need to returns the LiveData here’s the code.

      class Customer {
      int id;
      }
      class CustomerRepo {
      private CustomerDao customerDao;
      Customer findCustomer(int id) {
      return customerDao.findCustomer(id);
      }
      LiveData findCustomerInspectionChecks(int id) {
      return customerDao.inspectionCheckLists(id);
      }
      }
      class InspectionChecklistViewModel extends AndroidViewModel {
      private CustomerRepo customerRepo = new CustomerRepo();
      private MutableLiveData customerIdLiveData = new MutableLiveData<>();
      LiveData customerInspections = Transformations.switchMap(customerIdLiveData, customer -> customerRepo.findCustomerInspectionChecks(customer.id));
      public InspectionChecklistViewModel(@NonNull Application application) {
      super(application);
      }
      public void findCustomers(int id) {
      customerIdLiveData.setValue(customerRepo.findCustomer(id));
      }
      }
      interface CustomerDao {
      Customer findCustomer(int id);
      LiveData inspectionCheckLists(int customerId);
      }

      I write code from my understanding by looking at your code. If you still not able to understand you can reply here.

  2. Hi Saeed! I really appreciate your article. I’m green with android and trying to wrap my head around livedata connection and access. I have all the main parts (Room, Adapter, liveModel and Repository). I am coding to observe data from a List object but the return of the list object added is returning empty on observing LiveData. I have read many responses but not getting the answer I need. In my room, I have multiple DAOs for various tables, which created perfectly and prepopulated. I just want to access it. I can access dao function to access function data from the main thread but I want it from the LiveData. Can you explain it?

  3. Hello my Dear!
    Thankyou for yr article!
    You can help me?
    I have the error:cannot access database in the main thread!
    i want read a user from the database by using the arc components:
    my class dao:
    @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
    @Query(“SELECT * FROM user_table WHERE email=:email AND mdp=:mdp AND role=:role”)
    LiveData getUser(String email,String mdp,String role);
    and the repository:
    public LiveData getUser(String email,String mdp,String role)
    {
    user=userDao.getUser(email,mdp,role);
    return user;
    }
    and the main activity:
    userViewModel.getUser(log,mdpe,item).observe(this, new Observer() {
    @Override
    public void onChanged(@Nullable User user) {

    }
    });
    these instructions for validation inputs:
    if (user.getEmail().equals(login)&&user.getMdp().equals(mdp)&&user.getRole().equals(“Agent logistique”)) {
    ok = true;
    Intent intent = new Intent(MainActivity.this, Main2Activity.class);
    startActivity(intent);

    }
    i want do login app by avoiding this error!
    Thankyou!

    1. The same question is already asked by some person on Stack OverFlow. Here is the link. Still if your problem not solved, you can upload the code on GitHub so that I can review the code.

Comments are closed.