Platform:
- Android
Difficulty level:
- Beginner
IDE:
- Android Studio
Hello everyone!
The idea of this post is help the reader step by step to create a simple CRUD App for Android using SQLite where we are going to check a list, register, edit and delete characters. Reading this post you will be able to insert, update, delete and select datas in an Android Application. To enjoy 100% of this post is required the basic knowledge in Android language and at least the minimum of Object Orientation concept.
One of the first things that every Android Developer needs to dominate is SQLite. The main method to store local data for your Android Application.
SQLite is a light and simple (but fast and efficient) database present in all Android devices. In each app, it's a file inside /databases app's instalation folder (/data/data/APP_PACKAGE_NAME/databases/DATABASE_NAME).
Very well... Let's code!
1 - Create the Project
- Open Android Studio and Start a new project called SimpleCRUD and Next;
- Check only Phone and Tablet option with API 15. Next;
- Select Blank Activity with Fragments. Next;
- Don't alter the names of files and Finish.
2 - Add Floating Action Button
Only to create a good view for our app in a simple way :)
- Add as a dependency to your build.gradle:
dependencies {
compile 'com.melnykov:floatingactionbutton:1.3.0'
}
3 - Layout Time
Before the hard code, let's set up out xml files.
- First our first fragment with ListView to show our registers and the Floating Action Button for insert new items. Tip: you need an image to put on Action Button and Google provides a lot of icons here: https://www.google.com/design/icons/ in this example we'll use ic_add, download and copy the drawable content to your project into drawable folders).
fragment_main.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:fab="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.melnykov.fab.FloatingActionButton
android:id="@+id/actionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:layout_margin="16dp"
android:src="@drawable/ic_add"
fab:fab_colorNormal="#144266"
fab:fab_colorPressed="#ff15304c"
fab:fab_colorRipple="#ff3b7095" />
</FrameLayout>
- Second, the character fragment which we'll insert and edit characters.
fragment_character.xml
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:fab="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="5dp"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Name:" />
<EditText
android:id="@+id/edtName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPersonName" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Race:" />
<EditText
android:id="@+id/edtRace"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPersonName" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Sex:" />
<RadioGroup
android:id="@+id/rgSex"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RadioButton
android:id="@+id/rbSexMale"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Male" />
<RadioButton
android:id="@+id/rbSexFemale"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Female" />
</RadioGroup>
</LinearLayout>
</ScrollView>
- A little adjust in our main activity xml (our fragment container).
activity_main.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
- For last, our menu (Icons: ic_delete and ic_done).
menu_character.xml
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_delete"
android:title="Delete"
android:icon="@drawable/ic_delete"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_save"
android:title="Save"
android:icon="@drawable/ic_done"
app:showAsAction="ifRoom" />
</menu>
4 - Code Time!
Finally the time comes :)
- Let's start adapting the Main Activity, checking if the savedInstanceState (parameter that keep your Activity's data when you recreate it) is null to instantiate and allocate the Fragment only in the first time when the Activity is created.
MainActivity.java
package com.letscodeeveryday.simplecrud;
import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
public class MainActivity extends ActionBarActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if(savedInstanceState == null){
getSupportFragmentManager()
.beginTransaction()
.add(R.id.fragment, new MainFragment())
.commit();
}
}
}
- The second thing to do is create the class for our Characters who will contains an id, a name, a race and a sex. It's a simple class with getters and setters as you use to create in the beginning of your Object Orientation classes. The unique difference here is three addtional functions that I'll explain later (fromCursor, toValues and toString). I'm used to create a package called model inside the main package to put this kind of classes into (it's only for organization and is recommended).
model/Character.java
package com.letscodeeveryday.simplecrud.model;
import android.content.ContentValues;
import android.database.Cursor;
import com.letscodeeveryday.simplecrud.database.tables.TableCharacter;
public class Character {
private int id;
private String name;
private String race;
private String sex;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getRace() {
return race;
}
public void setRace(String race) {
this.race = race;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public Character fromCursor(Cursor c){
this.setId(c.getInt(c.getColumnIndex(TableCharacter.FIELD_ID)));
this.setName(c.getString(c.getColumnIndex(TableCharacter.FIELD_NAME)));
this.setRace(c.getString(c.getColumnIndex(TableCharacter.FIELD_RACE)));
this.setSex(c.getString(c.getColumnIndex(TableCharacter.FIELD_SEX)));
return this;
}
public ContentValues toValues() {
ContentValues cv = new ContentValues();
if(this.getId() > 0){
cv.put(TableCharacter.FIELD_ID, this.getId());
}
cv.put(TableCharacter.FIELD_NAME, this.getName());
cv.put(TableCharacter.FIELD_RACE, this.getRace());
cv.put(TableCharacter.FIELD_SEX, this.getSex());
return cv;
}
public String toString(){
return this.getName();
}
}
- Next step: a class for the character's table. It's important to centralize the table informations like the create statement and fields names and everytime that you need to use some reference about some field, you should use this class attributes. It makes the maintenence easier. In this step you can see a create statement example, it's like any SQL create, the peculiarity here is the data types: SQLite works only with NULL, INTEGER, REAL, TEXT and BLOB values. For more informations and tips of how to work with other data types like DATE you can check here: Datatypes In SQLite Version 3. I put it into package database/tables.
database/tables/TableCharacter.java
package com.letscodeeveryday.simplecrud.database.tables;
public class TableCharacter {
public static String TABLE_NAME = "character";
public static String FIELD_ID = "_id";
public static String FIELD_NAME = "name";
public static String FIELD_RACE = "race";
public static String FIELD_SEX = "sex";
public static String CREATE_STATEMENT =
"CREATE TABLE `" + TABLE_NAME + "` (" +
" `" + FIELD_ID + "` INTEGER PRIMARY KEY AUTOINCREMENT," +
" `" + FIELD_NAME + "` TEXT NOT NULL," +
" `" + FIELD_RACE + "` TEXT NOT NULL," +>
" `" + FIELD_SEX + "` TEXT NOT NULL" +
")";
}
- With our character and table class in hands let's go for the main class of this project... The SQLiteOpenHelper class! This is the class responsible for create the database, aplly the database version upgrades changes, offer the database object to interact with it. Everytime when this class is instantiated it'll check if the database file exists and if it's not then the class will call the onCreate function. If you increment the database version and run it in an device that contains an older version, the functions onUpgrade is called. In this example we are going to use only the onCreate (wi event and put the database name and version fixed.
database/MyOpenHelper.java
package com.letscodeeveryday.simplecrud.database;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import com.letscodeeveryday.simplecrud.database.tables.TableCharacter;
public class MyOpenHelper extends SQLiteOpenHelper{
public static String DATABASE_NAME = "SimpleCRUD";
public static int DATABASE_VERSION = 1;
public MyOpenHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(TableCharacter.CREATE_STATEMENT);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
- After of had our database created, we need to make actions with it. So, we create the Character DAO (Database Access Object). In this class where we will find actions like select, insert, update and delete for related to Character Table. This class uses MyOpenHelper class to get access to the database. For each action a connection to the database opens in beginning and close in the end. Here we can even see the functionality of the functions fromCursor and toValues create in Character class which the first one helps to create a character by Cursor (Object returned when you make a select and works like a cursor of any database: it's a pointer in the set returned by the query where you can move around the rows doing something) and toValues converts a character in a ContentValues (Object used to insert or update values in database) object. I create the package dao to put it.
dao/CharacterDAO.java
package com.letscodeeveryday.simplecrud.dao;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import com.letscodeeveryday.simplecrud.database.MyOpenHelper;
import com.letscodeeveryday.simplecrud.database.tables.TableCharacter;
import com.letscodeeveryday.simplecrud.model.Character;
import java.util.ArrayList;
public class CharacterDAO {
MyOpenHelper myOpenHelper;
public CharacterDAO(Context context){
myOpenHelper = new MyOpenHelper(context);
}
public ArrayList<Character> select(String where, String[] whereArgs){
ArrayList<Character> characters = new ArrayList<>();
SQLiteDatabase database = myOpenHelper.getReadableDatabase();
try{
Cursor c = database.query(TableCharacter.TABLE_NAME, new String[]{"*"}, where, whereArgs, null, null, TableCharacter.FIELD_NAME);
if(c.getCount() > 0 && c.moveToFirst()) {
while (!c.isAfterLast()) {
characters.add(new Character().fromCursor(c));
c.moveToNext();
}
c.close();
}
}catch (Exception e){
e.printStackTrace();
}
database.close();
return characters;
}
public long insert(Character c){
long id = -1;
SQLiteDatabase database = myOpenHelper.getWritableDatabase();
try {
id = database.insert(TableCharacter.TABLE_NAME, null, c.toValues());
}catch (Exception e){
e.printStackTrace();
}
database.close();
return id;
}
public int update(Character c){
int rows = -1;
SQLiteDatabase database = myOpenHelper.getWritableDatabase();
try {
ContentValues cv = c.toValues();
rows = database.update(TableCharacter.TABLE_NAME, cv, TableCharacter.FIELD_ID + " = ?", new String[]{String.valueOf(cv.getAsInteger(TableCharacter.FIELD_ID))});
}catch (Exception e){
e.printStackTrace();
}
database.close();
return rows;
}
public int delete(int id){
int rows = -1;
SQLiteDatabase database = myOpenHelper.getWritableDatabase();
try {
rows = database.delete(TableCharacter.TABLE_NAME, TableCharacter.FIELD_ID + " = ?", new String[]{String.valueOf(id)});
}catch (Exception e){
e.printStackTrace();
}
database.close();
return rows;
}
}
- Now is time to create (or edit if you already have it) the Main Fragment. This fragment will contain the list of characters and the awesome floating action button (hmmmmm... what a modern app) to add characters. When a list item is clicked we are redirected to the register fragment with loaded fields (next step is for this) where you can delete or edit the character. On onResume event we are going to populate the list, it's important to do it in this moment because if we do it on onCreate event, when we go to the second fragment and come back, the list will not get updated. Now is the time to explain the function toString created on Character class, that's necessary because we are using the class ArrayAdapter to populate our ListView and we are giving a ArrayList of Characters. So, the ArrayAdapter will try to convert the chacters to string to show it in ListView and if we had hadn't created the toString function it'd appears a strange thing like Character@blablabla...
MainFragment.java
package com.letscodeeveryday.simplecrud;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import com.letscodeeveryday.simplecrud.dao.CharacterDAO;
import com.letscodeeveryday.simplecrud.database.tables.TableCharacter;
import com.letscodeeveryday.simplecrud.model.Character;
import com.melnykov.fab.FloatingActionButton;
import java.util.ArrayList;
public class MainFragment extends Fragment {
FloatingActionButton actionButton;
ListView listView;
ArrayList<Character> characters = new ArrayList<>();
public MainFragment() {
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_main, container, false);
//find views
actionButton = (FloatingActionButton) view.findViewById(R.id.actionButton)
listView = (ListView) view.findViewById(R.id.listView);
//start listeners
actionButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
replaceFragment(null);
}
});
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Character character = characters.get(position);
Bundle bundle = new Bundle();
bundle.putInt(TableCharacter.FIELD_ID, character.getId());
replaceFragment(bundle);
}
});
return view;
}
@Override
public void onResume() {
//populate list
CharacterDAO characterDAO = new CharacterDAO(getActivity());
characters = characterDAO.select(null, null);
listView.setAdapter(new ArrayAdapter<>(getActivity(), android.R.layout.simple_list_item_1, characters));
super.onResume();
}
private void replaceFragment(Bundle args) {
CharacterFragment fragment = new CharacterFragment();
if (args != null) {
fragment.setArguments(args);
}
getActivity()
.getSupportFragmentManager()
.beginTransaction()
.addToBackStack(this.getClass().getName())
.replace(R.id.fragment, fragment)
.setTransitionStyle(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.commit();
}
}
- The last thing to do is the second fragment, where is possible to insert, update and delete a character. To recovery, update and delete character, it'is important to get the Character's id passed by the Fragment before using the getArguments method
CharacterFragment.java
package com.letscodeeveryday.simplecrud;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.Toast;
import com.letscodeeveryday.simplecrud.dao.CharacterDAO;
import com.letscodeeveryday.simplecrud.database.tables.TableCharacter;
import com.letscodeeveryday.simplecrud.model.Character;
import java.util.ArrayList;
public class CharacterFragment extends Fragment {
EditText edtName, edtRace;
RadioGroup rgSex;
public CharacterFragment() {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_character, container, false);
//find views
edtName = (EditText)view.findViewById(R.id.edtName);
edtRace = (EditText)view.findViewById(R.id.edtRace);
rgSex = (RadioGroup)view.findViewById(R.id.rgSex);
if(getArguments() != null){
recoveyCharacter();
}
setHasOptionsMenu(true);
return view;
}
private void recoveyCharacter(){
Bundle args = getArguments();
CharacterDAO characterDAO = new CharacterDAO(getActivity());
ArrayList<Character> characters = characterDAO.select(TableCharacter.FIELD_ID + " = ?", new String[]{String.valueOf(args.getInt(TableCharacter.FIELD_ID))});
if(characters.size() > 0){
Character character = characters.get(0);
edtName.setText(character.getName());
edtRace.setText(character.getRace());
if(character.getSex().equals("M")){
RadioButton rb = (RadioButton)rgSex.findViewById(R.id.rbSexMale);
rb.setChecked(true);
}else{
RadioButton rb = (RadioButton)rgSex.findViewById(R.id.rbSexFemale);
rb.setChecked(true);
}
}
}
private void saveCharacter(){
CharacterDAO characterDAO = new CharacterDAO(getActivity());
if(checkFields()) {
Character character = new Character();
character.setName(edtName.getText().toString().trim());
character.setRace(edtRace.getText().toString().trim());
switch (rgSex.getCheckedRadioButtonId()){
case R.id.rbSexMale:
character.setSex("M");
break;
case R.id.rbSexFemale:
character.setSex("F");
break;
}
if(getArguments() != null){
character.setId(getArguments().getInt(TableCharacter.FIELD_ID));
characterDAO.update(character);
}else{
characterDAO.insert(character);
}
popThisFragment();
}
}
private void deleteCharacter(){
CharacterDAO characterDAO = new CharacterDAO(getActivity());
characterDAO.delete(getArguments().getInt(TableCharacter.FIELD_ID));
popThisFragment();
}
private boolean checkFields(){
String error = "";
if(edtName.getText().toString().trim().length() < 1){
error += "- Name required\n";
}
if(edtRace.getText().toString().trim().length() < 1){
error += "- Race required\n";
}
switch (rgSex.getCheckedRadioButtonId()){
case R.id.rbSexMale:
case R.id.rbSexFemale:
default:
error += "- Sex required\n";
}
if(error.equals("")){
return true;
}
Toast.makeText(getActivity(), error, Toast.LENGTH_LONG).show();
return false;>
}
private void popThisFragment(){
getActivity().getSupportFragmentManager().popBackStack();
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.menu_character, menu);
if(getArguments() == null){
menu.findItem(R.id.action_delete).setVisible(false);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()){
case R.id.action_save:
saveCharacter();
break;
case R.id.action_delete:
deleteCharacter();
break;
}
return super.onOptionsItemSelected(item);
}
}
5 - Build and Run
Eyes of the Tiger!
- Yes great warrior... If you still are here you are a champion and congratulations! Your app is ready to run.
Do it and you will see something like this: