Difference between revisions of "An Android Room Database and Repository Tutorial"
(One intermediate revision by the same user not shown) | |||
Line 16: | Line 16: | ||
The user interface layout created in the previous chapter was the first step in creating a rudimentary inventory app designed to store the names and quantities of products. When completed, the app will provide the ability to add, delete and search for database entries while also displaying a scrollable list of all products currently stored in the database. This product list will update automatically as database entries are added or deleted. | The user interface layout created in the previous chapter was the first step in creating a rudimentary inventory app designed to store the names and quantities of products. When completed, the app will provide the ability to add, delete and search for database entries while also displaying a scrollable list of all products currently stored in the database. This product list will update automatically as database entries are added or deleted. | ||
− | == | + | == Modifying the Build Configuration == |
Begin by launching Android Studio and opening the RoomDemo project started in the previous chapter. Before adding any new classes to the project, the first step is to add some additional libraries to the build configuration, specifically the Room persistence library and the RecyclerView library. Locate and edit the module level build.gradle file (app -> Gradle Scripts -> build.gradle (Module: app)) and modify it as follows: | Begin by launching Android Studio and opening the RoomDemo project started in the previous chapter. Before adding any new classes to the project, the first step is to add some additional libraries to the build configuration, specifically the Room persistence library and the RecyclerView library. Locate and edit the module level build.gradle file (app -> Gradle Scripts -> build.gradle (Module: app)) and modify it as follows: | ||
Line 31: | Line 31: | ||
</pre> | </pre> | ||
− | == | + | == Building the Entity == |
This project will begin by creating the entity which defines the schema for the database table. The entity will consist of an integer for the product id, a string column to hold the product name and another integer value to store the quantity. The product id column will serve as the primary key and will be auto-generated. Table 68-2 summarizes the structure of the entity: | This project will begin by creating the entity which defines the schema for the database table. The entity will consist of an integer for the product id, a string column to hold the product name and another integer value to store the quantity. The product id column will serve as the primary key and will be auto-generated. Table 68-2 summarizes the structure of the entity: | ||
− | + | ||
+ | {| class="wikitable" | ||
+ | |- | ||
+ | ! Column !! Data Type | ||
+ | |- | ||
+ | | productid || Integer / Primary Key / Auto Increment | ||
+ | |- | ||
+ | | productname || String | ||
+ | |- | ||
+ | | productquantity || Integer | ||
+ | |} | ||
Line 382: | Line 392: | ||
import android.arch.lifecycle.LiveData; | import android.arch.lifecycle.LiveData; | ||
import android.arch.lifecycle.MutableLiveData; | import android.arch.lifecycle.MutableLiveData; | ||
− | |||
public class MainViewModel extends AndroidViewModel | public class MainViewModel extends AndroidViewModel |
Latest revision as of 18:52, 16 January 2019
Previous | Table of Contents | Next |
The Android Room Persistence Library | Accessing Cloud Storage using the Android Storage Access Framework |
This chapter will combine the knowledge gained in the chapter entitled The Android Room Persistence Library with the initial project created in the previous chapter to provide a detailed tutorial demonstrating how to implement SQLite-based database storage using the Room persistence library. In keeping with the Android architectural guidelines, the project will make use of a view model and repository. The tutorial will make use of all of the elements covered in The Android Room Persistence Library including entities, a Data Access Object, a Room Databases and asynchronous database queries.
About the RoomDemo Project
The user interface layout created in the previous chapter was the first step in creating a rudimentary inventory app designed to store the names and quantities of products. When completed, the app will provide the ability to add, delete and search for database entries while also displaying a scrollable list of all products currently stored in the database. This product list will update automatically as database entries are added or deleted.
Modifying the Build Configuration
Begin by launching Android Studio and opening the RoomDemo project started in the previous chapter. Before adding any new classes to the project, the first step is to add some additional libraries to the build configuration, specifically the Room persistence library and the RecyclerView library. Locate and edit the module level build.gradle file (app -> Gradle Scripts -> build.gradle (Module: app)) and modify it as follows:
dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'com.android.support:recyclerview-v7:28.0.0' implementation "android.arch.persistence.room:runtime:1.1.1" annotationProcessor "android.arch.persistence.room:compiler:1.1.1" . . }
Building the Entity
This project will begin by creating the entity which defines the schema for the database table. The entity will consist of an integer for the product id, a string column to hold the product name and another integer value to store the quantity. The product id column will serve as the primary key and will be auto-generated. Table 68-2 summarizes the structure of the entity:
Column | Data Type |
---|---|
productid | Integer / Primary Key / Auto Increment |
productname | String |
productquantity | Integer |
Add a class file for the entity by right clicking on the app -> java -> com.ebookfrenzy.roomdemo entry in the Project tool window and selecting the New -> Java Class menu option. In the Create New Class dialog, name the class Product and click on the OK button to generate the file.
When the Product.java file opens in the editor, modify it so that it reads as follows:
package com.ebookfrenzy.roomdemo; public class Product { private int id; private String name; private int quantity; public Product(String name, int quantity) { this.id = id; this.name = name; this.quantity = quantity; } public int getId() { return this.id; } public String getName() { return this.name; } public int getQuantity() { return this.quantity; } public void setId(int id) { this.id = id; } public void setName(String name) { this.name = name; } public void setQuantity(int quantity) { this.quantity = quantity; } }
The class now has variables for the database table columns and matching getter and setter methods. Of course this class does not become an entity until it has been annotated. With the class file still open in the editor, add annotations and corresponding import statements:
package com.ebookfrenzy.roomdemo; import android.support.annotation.NonNull; import android.arch.persistence.room.ColumnInfo; import android.arch.persistence.room.Entity; import android.arch.persistence.room.PrimaryKey; @Entity(tableName = "products") public class Product { @PrimaryKey(autoGenerate = true) @NonNull @ColumnInfo(name = "productId") private int id; @ColumnInfo(name = "productName") private String name; private int quantity; . . }
These annotations declare this as the entity for a table named products and assigns column names for both the id and name variables. The id column is also configured to be the primary key and auto-generated. Since a primary key can never be null, the @NonNull annotation is also applied. Since it will not be necessary to reference the quantity column in SQL queries, a column name has not been assigned to the quantity variable.
Creating the Data Access Object
With the product entity defined, the next step is to create the DAO interface. Referring once again to the Project tool window, right-click on the app -> java -> com.ebookfrenzy.roomdemo entry and select the New -> Java Class menu option. In the Create New Class dialog, enter ProductDao into the Name field and select Interface from the Kind menu as highlighted in Figure 68-1:
Click on OK to generate the new interface and, with the ProductDao.java file loaded into the code editor, make the following changes:
package com.ebookfrenzy.roomdemo; import java.util.List; import android.arch.lifecycle.LiveData; import android.arch.persistence.room.Dao; import android.arch.persistence.room.Insert; import android.arch.persistence.room.Query; @Dao public interface ProductDao { @Insert void insertProduct(Product product); @Query("SELECT * FROM products WHERE productName = :name") List<Product> findProduct(String name); @Query("DELETE FROM products WHERE productName = :name") void deleteProduct(String name); @Query("SELECT * FROM products") LiveData<List<Product>> getAllProducts(); }
The DAO implements methods to insert, find and delete records from the products database. The insertion method is passed a Product entity object containing the data to be stored while the methods to find and delete records are passed a string containing the name of the product on which to perform the operation. The getAllProducts() method returns a LiveData object containing all of the records within the database. This method will be used to keep the RecyclerView product list in the user interface layout synchronized with the database.
Adding the Room Database
The last task before adding the repository to the project is to implement the Room Database instance. Add a new class to the project named ProductRoomDatabase, this time with the Kind menu set to Class and the Abstract option enabled in the Modifiers section.
Once the file has been generated, modify it as follows using the steps outlined in the The Android Room Persistence Library chapter:
package com.ebookfrenzy.roomdemo; import android.content.Context; import android.arch.persistence.room.Database; import android.arch.persistence.room.Room; import android.arch.persistence.room.RoomDatabase; @Database(entities = {Product.class}, version = 1) public abstract class ProductRoomDatabase extends RoomDatabase { public abstract ProductDao productDao(); private static ProductRoomDatabase INSTANCE; static ProductRoomDatabase getDatabase(final Context context) { if (INSTANCE == null) { synchronized (ProductRoomDatabase.class) { if (INSTANCE == null) { INSTANCE = Room.databaseBuilder(context.getApplicationContext(), ProductRoomDatabase.class, "product_database").build(); } } } return INSTANCE; } }
Adding the Repository
Add a new class named ProductRepository to the project, with the Kind menu set to Class and None enabled in the Modifiers section of the Create New Class dialog.
The repository class will be responsible for interacting with the Room database on behalf of the ViewModel and will need to provide methods that use the DAO to insert, delete and query product records. With the exception of the getAllProducts() DAO method (which returns a LiveData object) these database operations will need to be performed on separate threads from the main thread using the AsyncTask class.
Remaining within the ProductRepository.java file, add the code for the search AsyncTask. Also add a method named asyncFinished() which will be called by the query AsyncTask to return the search results to the repository thread:
package com.ebookfrenzy.roomdemo; import android.os.AsyncTask; import java.util.List; import android.arch.lifecycle.MutableLiveData; public class ProductRepository { private MutableLiveData<List<Product>> searchResults = new MutableLiveData<>(); private void asyncFinished(List<Product> results) { searchResults.setValue(results); } private static class QueryAsyncTask extends AsyncTask<String, Void, List<Product>> { private ProductDao asyncTaskDao; private ProductRepository delegate = null; QueryAsyncTask(ProductDao dao) { asyncTaskDao = dao; } @Override protected List<Product> doInBackground(final String... params) { return asyncTaskDao.findProduct(params[0]); } @Override protected void onPostExecute(List<Product> result) { delegate.asyncFinished(result); } } }
The above declares a MutableLiveData variable named searchResults into which the results of a search operation are stored whenever an asynchronous search task completes (later in the tutorial, an observer within the ViewModel will monitor this live data object).
The AsyncTask class contains a constructor method into which must be passed a reference to the DAO object. The doInBackground() method is passed a String containing the product name for which the search is to be performed, passes it to the findProduct() method of the DAO and returns a list of matching Product entity objects which will, in turn, be passed to the onPostExecute() method. Finally, the onPostExecute() method stores the matching product list in the searchResults MutableLiveData object.
The repository will also need to include the following AsyncTask implementation for inserting products into the database:
private static class InsertAsyncTask extends AsyncTask<Product, Void, Void> { private ProductDao asyncTaskDao; InsertAsyncTask(ProductDao dao) { asyncTaskDao = dao; } @Override protected Void doInBackground(final Product... params) { asyncTaskDao.insertProduct(params[0]); return null; } }
Once again a constructor method is passed a reference to the DAO object, though this time the doInBackground() method is passed an array of Product entity objects to be inserted into the database. Since the app allows only one new product to be added at a time, the method simply inserts the first Product in the array into the database via a call to the insertProduct() DAO method. In this case, no results need to be returned from the task.
The only remaining AsyncTask will be used when deleting products from the database and should be added beneath the insertAsyncTask declaration as follows:
private static class DeleteAsyncTask extends AsyncTask<String, Void, Void> { private ProductDao asyncTaskDao; DeleteAsyncTask(ProductDao dao) { asyncTaskDao = dao; } @Override protected Void doInBackground(final String... params) { asyncTaskDao.deleteProduct(params[0]); return null; } }
With the AsyncTask classes defined, the repository class now needs to provide some methods that can be called by the ViewModel to initiate these operations. These methods will create and call appropriate AsyncTask instances and pass through a reference to the DAO. In order to be able to do this, however, the repository needs to obtain the DAO reference via a ProductRoomDatabase instance. Add a constructor method to the ProductRepository class to perform these tasks:
. . import android.app.Application; . . public class ProductRepository implements AsyncResult { private MutableLiveData<List<Product>> searchResults = new MutableLiveData<>(); private ProductDao productDao; public ProductRepository(Application application) { ProductRoomDatabase db; db = ProductRoomDatabase.getDatabase(application); productDao = db.productDao(); } . .
With a reference to DAO stored, the methods are ready to be added to the class file:
. . public void insertProduct(Product newproduct) { InsertAsyncTask task = new InsertAsyncTask(productDao); task.execute(newproduct); } public void deleteProduct(String name) { DeleteAsyncTask task = new DeleteAsyncTask(productDao); task.execute(name); } public void findProduct(String name) { QueryAsyncTask task = new QueryAsyncTask(productDao); task.delegate = this; task.execute(name); } . .
In the cases of the insertion and deletion methods, the appropriate AsyncTask instance is created and passed the necessary arguments. In the case of the findProduct() method, the delegate property of the class is set to the repository instance so that the asyncFinished() method can be called after the search completes.
One final task remains to complete the repository class. The RecyclerView in the user interface layout will need to be able to keep up to date the current list of products stored in the database. The ProductDao class already includes a method named getAllProducts() which uses a SQL query to select all of the database records and return them wrapped in a LiveData object. The repository needs to call this method once on initialization and store the result within a LiveData object that can be observed by the ViewModel and, in turn, by the UI controller. Once this has been set up, each time a change occurs to the database table the UI controller observer will be notified and the RecyclerView can be updated with the latest product list. Remaining within the ProductRepository.java file, add a LiveData variable and call to the DAO getAllProducts() method within the constructor:
. . import android.arch.lifecycle.LiveData; . . public class ProductRepository { private MutableLiveData<List<Product>> searchResults = new MutableLiveData<>(); private LiveData<List<Product>> allProducts; private ProductDao productDao; public ProductRepository(Application application) { ProductRoomDatabase db; db = ProductRoomDatabase.getDatabase(application); productDao = db.productDao(); allProducts = productDao.getAllProducts(); } . . }
To complete the repository, add methods that the ViewModel can call to obtain references to the allProducts and searchResults live data objects:
public LiveData<List<Product>> getAllProducts() { return allProducts; } public MutableLiveData<List<Product>> getSearchResults() { return searchResults; }
Modifying the ViewModel
The ViewModel is responsible for creating an instance of the repository and for providing methods and LiveData objects that can be utilized by the UI controller to keep the user interface synchronized with the underlying database. As implemented in ProductRepository.java, the repository constructor requires access to the application context in order to be able to get a Room Database instance. To make the application context accessible within the ViewModel so that it can be passed to the repository, the ViewModel needs to subclass AndroidViewModel instead of ViewModel. Begin, therefore, by editing the MainViewModel.java file (located in the Project tool window under app -> java -> com.ebookfrenzy.roomdemo -> ui.main) and changing the class to extend AndroidViewModel and to implement the default constructor:
package com.ebookfrenzy.roomdemo.ui.main; import android.app.Application; import com.ebookfrenzy.roomdemo.Product; import com.ebookfrenzy.roomdemo.ProductRepository; import java.util.List; import android.arch.lifecycle.AndroidViewModel; import android.arch.lifecycle.LiveData; import android.arch.lifecycle.MutableLiveData; public class MainViewModel extends AndroidViewModel { private ProductRepository repository; private LiveData<List<Product>> allProducts; private MutableLiveData<List<Product>> searchResults; public MainViewModel (Application application) { super(application); repository = new ProductRepository(application); allProducts = repository.getAllProducts(); searchResults = repository.getSearchResults(); } }
MutableLiveData<List<Product>> getSearchResults() { return searchResults; } LiveData<List<Product>> getAllProducts() { return allProducts; } public void insertProduct(Product product) { repository.insertProduct(product); } public void findProduct(String name) { repository.findProduct(name); } public void deleteProduct(String name) { repository.deleteProduct(name); }
Creating the Product Item Layout
The name of each product in the database will appear within the RecyclerView list in the main user interface. This will require a simple layout resource file containing a TextView to be used for each row in the list. Add this file now by right-clicking on the app -> res -> layout entry in the Project tool window and selecting the New -> Layout resource file menu option. Name the file product_list_item and change the root element to LinearLayout before clicking on OK to create the file and load it into the layout editor. With the layout editor in Design mode, drag a TextView object from the palette onto the layout where it will appear by default at the top of the layout:
With the TextView selected in the layout, use the Attributes tool window to set the ID of the view to product_row and the layout_height to 30dp. Select the LinearLayout entry in the Component Tree window and set the layout_height attribute to wrap_content.
Adding the RecyclerView Adapter
As outlined in detail in the chapter entitled Working with the RecyclerView and CardView Widgets, a RecyclerView instance requires an adapter class to provide the data to be displayed. Add this class now by right clicking on the app -> java -> com.ebookfrenzy.roomdemo -> ui.main entry in the Project tool window and selecting the New -> Java Class... menu option. In the Create New Class Dialog, name the class ProductListAdapter. With the resulting ProductListAdapter.java class loaded into the editor, implement the class as follows:
package com.ebookfrenzy.roomdemo; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import java.util.List; import android.support.v7.widget.RecyclerView; import com.ebookfrenzy.roomdemo.Product; import com.ebookfrenzy.roomdemo.R; public class ProductListAdapter extends RecyclerView.Adapter<ProductListAdapter.ViewHolder> { private int productItemLayout; private List<Product> productList; public ProductListAdapter(int layoutId) { productItemLayout = layoutId; } public void setProductList(List<Product> products) { productList = products; notifyDataSetChanged(); } @Override public int getItemCount() { return productList == null ? 0 : productList.size(); } @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = LayoutInflater.from( parent.getContext()).inflate(productItemLayout, parent, false); ViewHolder myViewHolder = new ViewHolder(view); return myViewHolder; } @Override public void onBindViewHolder(final ViewHolder holder, final int listPosition) { TextView item = holder.item; item.setText(productList.get(listPosition).getName()); } static class ViewHolder extends RecyclerView.ViewHolder { TextView item; ViewHolder(View itemView) { super(itemView); item = itemView.findViewById(R.id.product_row); } } }
Preparing the Main Fragment
The last remaining component to modify is the MainFragment class which needs to configure listeners on the Button views and observers on the live data objects located in the ViewModel class. Before adding this code, some preparation work needs to be performed to add some imports, variables and to obtain references to view ids. Edit the MainFragment.java file and modify it as follows:
. . import android.widget.EditText; import android.widget.TextView; . . public class MainFragment extends Fragment { private MainViewModel mViewModel; private ProductListAdapter adapter; private TextView productId; private EditText productName; private EditText productQuantity; . . @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); mViewModel = ViewModelProviders.of(this).get(MainViewModel.class); productId = getView().findViewById(R.id.productID); productName = getView().findViewById(R.id.productName); productQuantity = getView().findViewById(R.id.productQuantity); listenerSetup(); observerSetup(); recyclerSetup(); } . . }
At various stages in the code, the app will need to clear the product information displayed in the user interface. To avoid code repetition, add the following clearFields() convenience function:
private void clearFields() { productId.setText(""); productName.setText(""); productQuantity.setText(""); }
Before the app can be built and tested, the three setup methods called from the onActivityCreated() method above need to be added to the class.
Adding the Button Listeners
The user interface layout for the main fragment contains three buttons each of which needs to perform a specific task when clicked by user. Edit the MainFragment.java file and add the listenerSetup() method:
. . import com.ebookfrenzy.roomdemo.Product; import android.widget.Button; . . private void listenerSetup() { Button addButton = getView().findViewById(R.id.addButton); Button findButton = getView().findViewById(R.id.findButton); Button deleteButton = getView().findViewById(R.id.deleteButton); addButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { String name = productName.getText().toString(); String quantity = productQuantity.getText().toString(); if (!name.equals("") && !quantity.equals("")) { Product product = new Product(name, Integer.parseInt(quantity)); mViewModel.insertProduct(product); clearFields(); } else { productId.setText("Incomplete information"); } } }); findButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { mViewModel.findProduct(productName.getText().toString()); } }); deleteButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { mViewModel.deleteProduct(productName.getText().toString()); clearFields(); } }); } . . }
The addButton listener performs some basic validation to ensure that the user has entered both a product name and quantity and uses this data to create a new Product entity object (note that the quantity string is converted to an integer to match the entity data type). The ViewModel insertProduct() method is then called and passed the Product object before the fields are cleared.
The findButton and deleteButton listeners pass the product name to either the ViewModel findProduct() or deleteProduct() method.
Adding LiveData Observers
The user interface now needs to add observers to remain synchronized with the searchResults and allProducts live data objects within the ViewModel. Remaining in the Mainfragment.java file, implement the observer setup method as follows:
. . import android.arch.lifecycle.Observer; . . import java.util.List; import java.util.Locale; . . private void observerSetup() { mViewModel.getAllProducts().observe(this, new Observer<List<Product>>() { @Override public void onChanged(@Nullable final List<Product> products) { adapter.setProductList(products); } }); mViewModel.getSearchResults().observe(this, new Observer<List<Product>>() { @Override public void onChanged(@Nullable final List<Product> products) { if (products.size() > 0) { productId.setText(String.format(Locale.US, "%d", products.get(0).getId())); productName.setText(products.get(0).getName()); productQuantity.setText(String.format(Locale.US, "%d", products.get(0).getQuantity())); } else { productId.setText("No Match"); } } }); } . . }
The “all products” observer simply passes the current list of products to the setProductList() method of the RecyclerAdapter where the displayed list will be updated.
The “search results” observer checks that at least one matching result has been located in the database, extracts the first matching Product entity object from the list, gets the data from the object, converts it where necessary and assigns it to the TextView and EditText views in the layout. If the product search failed, the user is notified via a message displayed on the product ID TextView.
Initializing the RecyclerView
Add the final setup method to initialize and configure the RecyclerView and adapter as follows:
. . import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; . . private void recyclerSetup() { RecyclerView recyclerView; adapter = new ProductListAdapter(R.layout.product_list_item); recyclerView = getView().findViewById(R.id.product_recycler); recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); recyclerView.setAdapter(adapter); } . . }
Testing the RoomDemo App
Compile and run the app on a device or emulator, add some products and make sure that they appear automatically in the RecyclerView. Perform a search for an existing product and verify that the product ID and quantity fields update accordingly. Finally, enter the name for an existing product, delete it from the database and confirm that it is removed from the RecyclerView product list.
Summary
This chapter has demonstrated the use of the Room persistence library to store data in a SQLite database. The finished project made use of a repository to separate the ViewModel from all database operations and demonstrated the creation of entities, a DAO and a room database instance, including the use of asynchronous tasks when performing some database operations.
Previous | Table of Contents | Next |
The Android Room Persistence Library | Accessing Cloud Storage using the Android Storage Access Framework |