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:
You can add and remove data from database and display it on the screen, like you want.
We need data class DataItem
:
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:
implementation 'com.android.support:cardview-v7:28.0.0'
Fragment_item.xml:
="1.0"="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>
To make red round Textview
with numbers, we need round_shape.xml:
="1.0"="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:
android:background="@drawable/round_shape"
Add button to fragment_list.xml:
="1.0"="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>
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.
Add some to build.gradle
of app module:
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"
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:
@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
.
@Dao
public interface DataDAO {
@Insert(onConflict = IGNORE)
void insertItem(DataItem item);
@Delete
void deleteItem(DataItem person);
@Query("DELETE FROM dataitem WHERE id = :itemId")
void deleteByItemId(long itemId);
@Query("SELECT * FROM DataItem")
LiveData<List<DataItem>> getAllData();
@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
:
@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())
.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:
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;
}
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:
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.
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
.
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 (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();
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);
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);
viewModel = ViewModelProviders.of(this).get(DataViewModel.class);
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 {
void onListClickItem(DataItem dataItem);
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.
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) {
id_data = bundle.getLong(ID_LONG);
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) {
mListener.onDialogPositiveClick(AlertDialogFragment.this, id_data);
}
})
.setNegativeButton(R.string.message_no, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
mListener.onDialogNegativeClick(AlertDialogFragment.this);
}
});
return builder.create();
}
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
public void onAttach(Context context) {
super.onAttach(context);
try {
mListener = (AlertDialogListener) context;
} catch (ClassCastException e) {
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.