Data Storage- SQLite & Content Provides
Topics covered: Data storage into DB, cursor adaptor
In this article, we will talk about the importance of data storage and how it can be implemented in android. You can find the working of a few apps in the below video that made use of data storage for the functioning of the app. Documentation on applications petInventory and Store inventory are in the links below.
Pet inventory: https://github.com/nikitha2/PetsShelterApp.git
Store inventory:https://github.com/nikitha2/InventoryApp.git
Store Inventory / Pet Inventory overview
Let’s consider the inventory app to understand the concept. The Pet Inventory/ Store Inventory app will help us understand data storage on mobile DB. The app starts off with an empty list of pets/inventories. As we add to the inventory, DB is populated. We can add dummy data to the list, add a new record of a pet, or edit an already existing entry. We can future delete a record or delete all records if needed. When we close the app and reopen we will still be able to see the data as it is stored in the database.
What’s the purpose of the database?
Now that we know how the app functions. Let us consider a scenario where we are not using a DB and we add an entry to the list of pets. What do you think will happen when the screen is refreshed?
The list will be empty again as we did not store the data anywhere so we do not have a way to load the data to the screen. Therefore data storage holds an important role in every app. Data storage can be a database on our mobile/cloud or a file on mobile/cloud or any form of data storage. Either way, we store the data that we can use later.
In my article, we will particularly talk about performing CRUD operations on a database on the android device.
Different layers to do a DB call
For a DB call to be done the query needs to go through multiple layers as shown below. Let’s look at each layer and the purpose.
Database: Android uses SQLiteDatabase.
Contract: Extends BaseColumns class. The contract class is used to keep things uniform. Each table in the database has a subclass in the contract class. These subclasses have the column names and other frequently used string as constants so that every time a CRUD operation is performed we can use the constants from the contracts class. This reduces the scope of error and increases usability and uniform ability of code.
package com.nikitha.android.pets.Data;import android.content.ContentResolver;
import android.net.Uri;
import android.provider.BaseColumns;public final class PetsContract { public PetsContract() {
} public static final class PetsEntry implements BaseColumns {public final static String DATABASE_NAME ="PetsData.db";
public final static String TABLE_NAME ="Pets";
//public final static String COLUMN_PET_ID="id";
public final static String COLUMN_PET_NAME="Name";
public final static String COLUMN_PET_BREED="Breed";
public final static String COLUMN_PET_WEIGHT="Weight";
public final static String COLUMN_PET_GENDER="Gender"; public final static int gender_Pet_Unknown=0;
public final static int gender_Pet_Male=1;
public final static int gender_Pet_Female=2;
public static final String CONTENT_AUTHORITY = "com.nikitha.android.pets.Data";
/**
* To make this a usable URI, we use the parse method which takes in a URI string and returns a Uri.
*/
public static final Uri BASE_CONTENT_URI = Uri.parse("content://" + CONTENT_AUTHORITY);/***The MIME type of the {@link #CONTENT_URI} for a list of pets*/
public static final String CONTENT_LIST_TYPE =
ContentResolver.CURSOR_DIR_BASE_TYPE + "/" + CONTENT_AUTHORITY + "/" + TABLE_NAME;/** The MIME type of the {@link #CONTENT_URI} for a single pet.*/
public static final String CONTENT_ITEM_TYPE =
ContentResolver.CURSOR_ITEM_BASE_TYPE + "/" + CONTENT_AUTHORITY + "/" + TABLE_NAME;
}
}
DB Helper: Extends SQLiteOpenHelper. Db helper is the only class that has to talk to the database directly and creates a DB if not already created. It overrides methods to perform CRUD operations. In the onCreate method, we create a database if the database is not already created.
package com.nikitha.android.pets.Data;import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import androidx.annotation.Nullable;
import static com.nikitha.android.pets.Data.Constants.*;
import static com.nikitha.android.pets.Data.PetsContract.PetsEntry.*;public class PetsDbHelper extends SQLiteOpenHelper {
public PetsDbHelper(@Nullable Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
} @Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(SQL_CREATE_PETS_TABLE);
} @Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
DATABASE_VERSION =newVersion;
db.execSQL(SQL_DELETE_PETS_TABLE);
onCreate(db);
} @Override
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
onUpgrade(db, oldVersion, newVersion);
} public void deleteAllEntries(SQLiteDatabase db){
db.execSQL(SQL_DELETE_ALL_FROM_TABLE);
}
}
Content Provider: This is the most important layer. The content provider is the part exposed to other apps. If another app wants to access the database of my present app, the content provider is through which they can access the database.
public class PetProvider extends ContentProvider {@Override
public boolean onCreate() {
petsDbHelper=new PetsDbHelper(getContext());
db = petsDbHelper.getWritableDatabase();
return false;
}@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
match=sUriMatcher.match(uri);
switch(match) {
case PETS: //perform task;break;
case PETS_ID://perform task;break;
default:cursor=null;break;
}
/** Set notification URI on the Cursor so we know what content URI the Cursor was created for.If the data at this URI changes, then we know we need to update the Cursor.**/
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}/**
* Returns the MIME type of data for the content URI.
*/
@Nullable
@Override
public String getType(@NonNull Uri uri) {
match=sUriMatcher.match(uri);
final int match = sUriMatcher.match(uri);
switch (match) {
case PETS:return CONTENT_LIST_TYPE;
case PETS_ID:return CONTENT_ITEM_TYPE;
default:throw new IllegalStateException("Unknown URI " + uri + " with match " + match);
}
}/**
* Insert new data into the provider with the given ContentValues.
*/
@RequiresApi(api = Build.VERSION_CODES.Q)
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
match=sUriMatcher.match(uri);
newId=db.insert(TABLE_NAME, null, values);
newUri = ContentUris.withAppendedId(uri, newId);
return newUri;
}}
Content Resolver: Content resolver analyses the URIs from all applications and decide what content provider the request is for. The need for content resolver comes into the picture because mobiles will have more than one app and each app might have a content provider of its own.
Performing CRUD operations from UI(Activity/Fragment)
Uri uri=Uri.parse(URI);//URI declared in contract
ContentValues value=new ContentValues();
value.put(PRODUCT_QUANTITY, (quan)); //Integer.toString
String id = (String)v.getTag();
String selection = _ID + "=?";
String[] selectionArgs = new String[]{id};
String[] columns={PRODUCT_QUANTITY};C: getContentResolver().insert(uri, values);R: getContext().getContentResolver().query(uri, columns, selection,
selectionArgs, null);
U:getContext().getContentResolver().update(uri,value,selection,selec
tionArgs);D: getContentResolver().delete(uri, null, null);
How can app1 get access to the app2 database?
If an app1 needs to access the database of the app 2. It requires permission. app1 manifest file should request permission similar to the below statement.
app1 Manifest:
<uses-permission android:name="com.example.udacity.droidtermsexample.TERMS_READ"/>app2 Manifest
<provider
android:name=".Data.InventoryProvider"
android:authorities="com.nikitha.android.inventoryapp.Data"
android:exported="false" />
Conclusion:
Room is an object-mapping library that provides local data persistence with minimal boilerplate code. At compile time, it validates each query against your data schema, so broken SQL queries result in compile-time errors instead of runtime failures. Room abstracts away some of the underlying implementation details of working with raw SQL tables and queries. It also allows you to observe changes to the database’s data, including collections and join queries, exposing such changes using LiveData objects. It even explicitly defines execution constraints that address common threading issues, such as accessing storage on the main thread.