Click here to Skip to main content
15,881,719 members
Articles / Mobile Apps / Android

Work with Database using Room and recyclerview in Android Applications

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
23 Feb 2019CPOL5 min read 37.8K   2.6K   2  
This article describes how to work with database in Android using Room and Recyclerview, also ViewModel.

Introduction

This article solves typical tasks:

  • store data in application - using Room
  • show data to user - using fragments and recyclerview
  • store and automatically update data using ViewModel

Background

Room provides an abstraction layer over SQLite to allow fluent database access while harnessing the full power of SQLite. The app uses the Room database to get the data access objects, or DAOs, associated with that database. The app then uses each DAO to get entities from the database and save any changes to those entities back to the database. Finally, the app uses an entity to get and set values that correspond to table columns within the database.

In the RecyclerView widget, several different components work together to display your data (list of objects). The overall container for your user interface is a RecyclerView object that you add to your layout. The RecyclerView fills itself with views provided by a layout manager that you provide. You can use one of our standard layout managers (such as LinearLayoutManager or GridLayoutManager), or implement your own.

ViewModel is a class that is responsible for preparing and managing the data for an Activity or a Fragment. It also handles the communication of the Activity / Fragment with the rest of the application. In other words, this means that a ViewModel will not be destroyed if its owner is destroyed for a configuration change (e.g. rotation). The new instance of the owner will just re-connect to the existing ViewModel.

Using the Code

Let's start! Create a new project in Android studio (I used version 3.2.1) with empty activity or you can download the source files and choose: File-New-Import project. We'll build an application like this:

Image 1

You can add and remove data from database and display it on the screen, like you want.

We need data class DataItem:

Java
public class DataItem {
    private long id;
    private String name;
    private String content;
    private String details;
    private String section;
}

It's class - our data to store in database. To show this data, we use the RecyclerView widget. Create new fragment: File-New-Fragment-Fragment(list). RecyclerView uses two XML files: one file represents item of the list and the second file represents a full list of item. Make some changes to fragment_item: add CardView widget and make custom Textview element. Also add to build.gradle for Cardview widget:

Java
//for recyclerview items
implementation 'com.android.support:cardview-v7:28.0.0'

Fragment_item.xml:

XML
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <android.support.v7.widget.CardView
        android:id="@+id/card_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"

        android:layout_margin="2dp"
        app:cardCornerRadius="2dp"
        app:cardElevation="3dp"
        app:cardUseCompatPadding="true">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:orientation="horizontal">

            <TextView
                android:id="@+id/item_number"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_margin="@dimen/text_margin"
                android:background="@drawable/round_shape"
                android:gravity="center"
                android:text="1"
                android:textAppearance="?attr/textAppearanceListItem"
                android:textColor="@color/colorWhite" />

            <TextView
                android:id="@+id/item_name"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_margin="@dimen/text_margin"
                android:text="text"
                android:textAppearance="?attr/textAppearanceListItem" />

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:gravity="end"
                android:orientation="horizontal">

                <TextView
                    android:id="@+id/item_section"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    android:layout_margin="@dimen/text_margin"
                    android:text="section"
                    android:textAppearance="?attr/textAppearanceListItem" />

                <ImageView
                    android:id="@+id/image_delete"
                    android:layout_width="40dp"
                    android:layout_height="40dp"
                    android:padding="8dp"
                    android:src="@drawable/ic_delete2" />
            </LinearLayout>

        </LinearLayout>
    </android.support.v7.widget.CardView>
</LinearLayout>

Image 2

To make red round Textview with numbers, we need round_shape.xml:

XML
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">
    <solid android:color="#FF0000" />
    <size
        android:width="30dp"
        android:height="30dp" />
</shape>

And set value of background our TextView to round_shape.xml:

Java
android:background="@drawable/round_shape"

Add button to fragment_list.xml:

XML
<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <Button
            android:id="@+id/add_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="end"
            android:layout_margin="@dimen/list_margin"
            android:text="Add" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/list"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="@dimen/list_margin"
            android:layout_marginRight="@dimen/list_margin"
            app:layoutManager="android.support.v7.widget.LinearLayoutManager"
            tools:context=".fragment.ListFragment"
            tools:listitem="@layout/fragment_item" />
    </LinearLayout>
</LinearLayout>

Image 3

Let's move to the database. This app focuses on a subset of the components, namely LiveData, ViewModel and Room. This diagram shows a basic form of this architecture. You can read more about this here.

Image 4

Add some to build.gradle of app module:

Java
// Room components
    implementation "android.arch.persistence.room:runtime:1.1.1"
    annotationProcessor "android.arch.persistence.room:compiler:1.1.1"
    androidTestImplementation "android.arch.persistence.room:testing:1.1.1"

// Lifecycle components
    implementation "android.arch.lifecycle:extensions:1.1.1"
    annotationProcessor "android.arch.lifecycle:compiler:1.1.1"

To make the DataItem 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 = "data_item_table")
    Each @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
    Every entity needs a primary key. To keep things simple, each word acts as its own primary key.
  • @NonNull
    Denotes that a parameter, field, or method return value can never be null.
  • @ColumnInfo(name = "name")
    Specify the name of the column in the table if you want it to be different from the name of the member variable.
  • Every field that's stored in the database needs to be either public or have a "getter" method. This sample provides a geName() method.

Change our class to this:

Java
@Entity
public class DataItem {
    @PrimaryKey(autoGenerate = true)
    @NonNull
    private long id;
    private String name;
    private String content;
    private String details;
    private String section;

    @Ignore
    public DataItem(String name, String content, String details, String section) {
        this.name = name;
        this.content = content;
        this.details = details;
        this.section = section;
    }

    public void setId(long id) {
        this.id = id;
    }

    public long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getContent() {
        return content;
    }

    public String getDetails() {
        return details;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public void setDetails(String details) {
        this.details = details;
    }

    public String getSection() {
        return section;
    }

    public void setSection(String section) {
        this.section = section;
    }

    public DataItem() {
        this.name = "name";
        this.content = "content";
        this.details = "details";
        this.section = "section";
    }
}

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, @Delete,@Query. The DAO must be an interface or abstract class. By default, all queries must be executed on a separate thread. Ok, make our own DAO interface.

Java
@Dao
public interface DataDAO {

    //Insert one item
    @Insert(onConflict = IGNORE)
    void insertItem(DataItem item);

    // Delete one item
    @Delete
    void deleteItem(DataItem person);

    //Delete one item by id
    @Query("DELETE FROM dataitem WHERE id = :itemId")
    void deleteByItemId(long itemId);

    //Get all items
    @Query("SELECT * FROM DataItem")
    LiveData<List<DataItem>> getAllData();

    //Delete All
    @Query("DELETE FROM DataItem")
    void 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. 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.

Next, create database class based on RoomDatabase:

Java
@Database(entities = {DataItem.class}, version = 1, exportSchema = false)
public abstract class DataRoomDbase extends RoomDatabase {

    private static DataRoomDbase INSTANCE;

    public abstract DataDAO dataDAO();

    public static DataRoomDbase getDatabase(Context context) {
        if (INSTANCE == null) {
            INSTANCE = Room.databaseBuilder(context.getApplicationContext(), 
                       DataRoomDbase.class, DataRoomDbase.class.getName())
                    //if you want create db only in memory, not in file
                    //Room.inMemoryDatabaseBuilder
                    //(context.getApplicationContext(), DataRoomDbase.class)
                    .build();
        }
        return INSTANCE;
    }

    public static void destroyInstance() {
        INSTANCE = null;
    }
}

Next move, create repository. A Repository is a class that 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 handles data operations. It provides a clean API to the rest of the app for app data. Make like this:

Java
public class DataRepository {
    private DataDAO mDataDao;
    private LiveData<List<DataItem>> mAllData;

    public DataRepository(Application application) {
        DataRoomDbase dataRoombase = DataRoomDbase.getDatabase(application);
        this.mDataDao = dataRoombase.dataDAO();
        this.mAllData = mDataDao.getAllData();
    }

    LiveData<List<DataItem>> getAllData() {
        return mAllData;
    }

// You must call this on a non-UI thread or your app will crash

    public void insert(DataItem dataItem) {
        new insertAsyncTask(mDataDao).execute(dataItem);
    }

    private static class insertAsyncTask extends AsyncTask<DataItem, Void, Void> {
        private DataDAO mAsyncTaskDao;
        insertAsyncTask(DataDAO dao) {
            mAsyncTaskDao = dao;
        }

        @Override
        protected Void doInBackground(final DataItem... params) {
            mAsyncTaskDao.insertItem(params[0]);
            return null;
        }
    }

    public void deleteItem(DataItem dataItem) {
        new deleteAsyncTask(mDataDao).execute(dataItem);
    }

    private static class deleteAsyncTask extends AsyncTask<DataItem, Void, Void> {
        private DataDAO mAsyncTaskDao;
        deleteAsyncTask(DataDAO dao) {
            mAsyncTaskDao = dao;
        }

        @Override
        protected Void doInBackground(final DataItem... params) {
            mAsyncTaskDao.deleteItem(params[0]);
            return null;
        }
    }

    public void deleteItemById(Long idItem) {
        new deleteByIdAsyncTask(mDataDao).execute(idItem);
    }

    private static class deleteByIdAsyncTask extends AsyncTask<Long, Void, Void> {
        private DataDAO mAsyncTaskDao;
        deleteByIdAsyncTask(DataDAO dao) {
            mAsyncTaskDao = dao;
        }

        @Override
        protected Void doInBackground(final Long... params) {
            mAsyncTaskDao.deleteByItemId(params[0]);
            return null;
        }
    }
}

Create new class based on ViewModel class:

Java
public class DataViewModel extends AndroidViewModel {

    private DataRepository mDataRepository;
    private LiveData<List<DataItem>> mListLiveData;

    public DataViewModel(@NonNull Application application) {
        super(application);
        mDataRepository = new DataRepository((application));
        mListLiveData = mDataRepository.getAllData();
    }

    public LiveData<List<DataItem>> getAllData() {
        return mListLiveData;
    }

    public void insertItem(DataItem dataItem) {
        mDataRepository.insert(dataItem);
    }

    public void deleteItem(DataItem dataItem) {
        mDataRepository.deleteItem(dataItem);
    }

    public void deleteItemById(Long idItem) {
        mDataRepository.deleteItemById(idItem);
    }
}

A ViewModel holds your app's UI data in a lifecycle 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:

  • You can put an observer on the data (instead of polling for changes) and only update the UI when the data actually changes.
  • The Repository and the UI are completely separated by the ViewModel. There are no database calls from the ViewModel, making the code more testable.
Java
public class MainActivity extends AppCompatActivity 
         implements ListFragment.OnListFragmentInteractionListener,
        AlertDialogFragment.AlertDialogListener {

    private DataViewModel mDataViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mDataViewModel = ViewModelProviders.of(this).get(DataViewModel.class);
        getSupportFragmentManager()
                .beginTransaction()
                .replace(R.id.main_layout, new ListFragment())
                .addToBackStack("list")
                .commit();
    }

    @Override
    public void onListClickItem(DataItem dataItem) {
        Toast.makeText(this, dataItem.getDetails(), Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onListFragmentDeleteItemById(long idItem) {
        Bundle bundle = new Bundle();
        bundle.putLong(ID_LONG, idItem);

        AlertDialogFragment alertDialogFragment = new AlertDialogFragment();
        alertDialogFragment.setArguments(bundle);
        alertDialogFragment.show(getSupportFragmentManager(), "Allert");
    }

    @Override
    public void onDialogPositiveClick(DialogFragment dialog, long idItem) {
        mDataViewModel.deleteItemById(idItem);
        Toast.makeText(this, getString(R.string.message_delete), Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onDialogNegativeClick(DialogFragment dialog) {
        Toast.makeText(this, getString(R.string.message_cancel), Toast.LENGTH_SHORT).show();

    }
}

Use ViewModelProviders to associate your ViewModel with your UI controller. When your app 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.

Java
public class ListFragment extends Fragment {

    private DataViewModel viewModel;
    private List<DataItem> mDataItemList;
    private ListRecyclerViewAdapter mListAdapter;
    private OnListFragmentInteractionListener mListener;

    public void setListData(List<DataItem> dataItemList) {
        //if data changed, set new list to adapter of recyclerview

        if (mDataItemList == null) {
            mDataItemList = new ArrayList<>();
        }
        mDataItemList.clear();
        mDataItemList.addAll(dataItemList);

        if (mListAdapter != null) {
            mListAdapter.setListData(dataItemList);
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_list, container, false);
        Context context = view.getContext();
        //set recyclerview
        RecyclerView recyclerView = view.findViewById(R.id.list);
        recyclerView.setLayoutManager(new LinearLayoutManager(context));
        mListAdapter = new ListRecyclerViewAdapter(mListener);

        if (mDataItemList != null) {
            mListAdapter.setListData(mDataItemList);
        }
        recyclerView.setAdapter(mListAdapter);

        //Add new item to db
        Button addButton = (Button) view.findViewById(R.id.add_button);
        addButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                viewModel.insertItem(new DataItem());

            }
        });

        return view;
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        //get viewmodel
        viewModel = ViewModelProviders.of(this).get(DataViewModel.class);
        //bind to Livedata
        viewModel.getAllData().observe(this, new Observer<List<DataItem>>() {
            @Override
            public void onChanged(@Nullable List<DataItem> dataItems) {
                if (dataItems != null) {
                    setListData(dataItems);
                }
            }
        });
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        if (context instanceof OnListFragmentInteractionListener) {
            mListener = (OnListFragmentInteractionListener) context;
        } else {
            throw new RuntimeException(context.toString()
                    + " must implement OnListFragmentInteractionListener");
        }
    }

    @Override
    public void onDetach() {
        super.onDetach();
        mListener = null;
    }
    
    public interface OnListFragmentInteractionListener {
        //onClick items of list
        void onListClickItem(DataItem dataItem);

        //onClick delete item from list
        void onListFragmentDeleteItemById(long idItem);
    }
}

Also, to display alert dialog, when you want to delete data from database, I made a new class - AlertDialogFragment (if you want, read this). This prevent memory leaks and uses lifecycle in a way that survives configuration changes.

Image 5

Image 6

Java
public class AlertDialogFragment extends DialogFragment {

    AlertDialogListener mListener;
    public static String ID_LONG = "ID_LONG";
    long id_data;

    public interface AlertDialogListener {
        void onDialogPositiveClick(DialogFragment dialog, long idItem);

        void onDialogNegativeClick(DialogFragment dialog);
    }

    @Override
    @NonNull
    public Dialog onCreateDialog(Bundle savedInstanceState) {

        Activity activity = getActivity();
        Bundle bundle = getArguments();

        if (bundle != null && activity != null) {
            //get data from bundle
            id_data = bundle.getLong(ID_LONG);
            // Use the Builder class for convenient dialog construction
            AlertDialog.Builder builder = new AlertDialog.Builder(activity);
            builder.setMessage(R.string.message_dialog)
                    .setPositiveButton(R.string.message_yes, new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog, int id) {
                            // set listener for yes button
                            mListener.onDialogPositiveClick(AlertDialogFragment.this, id_data);
                        }
                    })
                    .setNegativeButton(R.string.message_no, new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog, int id) {
                            // User cancelled the dialog
                            mListener.onDialogNegativeClick(AlertDialogFragment.this);
                        }
                    });
            // Create the AlertDialog object and return it
            return builder.create();
        }
        //if no bundle - show error
        AlertDialog.Builder builder = new AlertDialog.Builder(activity)
                .setNegativeButton(R.string.message_error, new DialogInterface.OnClickListener() {

                    @Override
                    public void onClick(DialogInterface dialog, int which) {

                    }
                });
        return builder.create();
    }

    // Override the Fragment.onAttach() method to instantiate the DialogListener

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        // Verify that the host activity implements the callback interface
        try {
            // Instantiate the DialogListener so we can send events to the host
            mListener = (AlertDialogListener) context;
        } catch (ClassCastException e) {
            // The activity doesn't implement the interface, throw exception
            throw new ClassCastException(context.toString()
                    + " must implement AlertDialogListener");
        }
    }
}

I hope this simple article will help you. You can easily improve this application. I like to develop applications, so you can try some of them here.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Junior)
Ukraine Ukraine
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --