Unit 5 - Data Storage Techniques

A complete coverage of Data Storage Techniques in Android - Shared Preferences, Files and Directories, SQLite Database, and Content Providers.

jinansh
Listen
12

Data Storage in Android

Introduction

Every app needs to remember something. A login app needs to remember if the user is logged in. A notes app needs to save what the user wrote. A music app needs to remember the user's volume setting.

But where does all this data go? Your app can't just keep it in variables — those disappear the moment the app closes.

Android provides multiple ways to store data, and each one is suited to a different type of data and use case.

Think of it like this:

  • Shared Preferences: A sticky note on your desk — small, quick reminders
  • Files and Directories: A folder in your drawer — store documents of any size
  • SQLite Database: A filing cabinet with organized folders — structured, searchable data
  • Content Providers: A shared library — other apps can access data through a common system

When to Use What

Storage Type Best For Data Size Accessible by Other Apps?
Shared Preferences Settings, login status, simple flags Small (key-value pairs) No
Files and Directories Text files, images, downloaded data Medium to Large Optional
SQLite Database Structured data like users, products, orders Medium to Large No (unless via Content Provider)
Content Providers Sharing data between apps Any Yes

MCQ

Which storage technique in Android is best suited for storing a user's login status?

  • SQLite Database
  • Content Provider
  • Shared Preferences
  • Files and Directories

Shared Preferences

What Are Shared Preferences?

Imagine you open an app and set dark mode on. You close the app and reopen it — dark mode is still on. How? The app saved your preference using Shared Preferences.

Shared Preferences is Android's simplest data storage method. It stores small amounts of data as key-value pairs in an XML file on the device. There's no database, no tables, no complex setup — just a key and its value.

Think of it like a dictionary:

  • Key: "dark_mode"Value: true
  • Key: "username"Value: "Arjun"
  • Key: "volume_level"Value: 75

What Can It Store?

Shared Preferences can only store primitive data types:

Data Type Example
String Username, email
Int Age, score, count
Boolean Is logged in? Dark mode on?
Float Volume level, rating
Long Timestamps

It cannot store complex objects like lists, images, or custom classes.

How It Works — Step by Step

Step 1: Get the SharedPreferences Object

val sharedPreferences = getSharedPreferences("MyAppPrefs", MODE_PRIVATE)
  • "MyAppPrefs" — the name of the preference file (you choose this)
  • MODE_PRIVATE — only your app can read/write this file

Step 2: Write Data (Save)

To save data, you need an Editor object:

val editor = sharedPreferences.edit()
editor.putString("username", "Arjun")
editor.putBoolean("is_logged_in", true)
editor.putInt("age", 21)
editor.apply()  // saves changes in the background
  • editor.apply() — saves asynchronously (recommended)
  • editor.commit() — saves synchronously, returns true or false

apply vs commit: Use apply() for most cases since it doesn't block the main thread. Use commit() only when you need immediate confirmation.

Step 3: Read Data (Retrieve)

val username = sharedPreferences.getString("username", "Guest")
val isLoggedIn = sharedPreferences.getBoolean("is_logged_in", false)
val age = sharedPreferences.getInt("age", 0)

The second argument is the default value — returned if the key doesn't exist yet.

Step 4: Delete Data

val editor = sharedPreferences.edit()
editor.remove("username")   // remove one key
editor.clear()              // remove all keys
editor.apply()

Complete Example — Login Status

// Save login state when user logs in
fun saveLoginState(username: String) {
    val prefs = getSharedPreferences("MyAppPrefs", MODE_PRIVATE)
    val editor = prefs.edit()
    editor.putBoolean("is_logged_in", true)
    editor.putString("username", username)
    editor.apply()
}

// Check login state when app opens
fun isUserLoggedIn(): Boolean {
    val prefs = getSharedPreferences("MyAppPrefs", MODE_PRIVATE)
    return prefs.getBoolean("is_logged_in", false)
}

// Clear on logout
fun logout() {
    val prefs = getSharedPreferences("MyAppPrefs", MODE_PRIVATE)
    prefs.edit().clear().apply()
}

Where Is the Data Stored?

The XML file is saved at:

/data/data/com.yourapp.package/shared_prefs/MyAppPrefs.xml

It looks like this:

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <boolean name="is_logged_in" value="true" />
    <string name="username">Arjun</string>
    <int name="age" value="21" />
</map>

Security Note: Shared Preferences stores data as plain XML. Never store passwords, tokens, or sensitive data here. Use EncryptedSharedPreferences from the AndroidX Security library for sensitive information.

MCQ

What is the default value used for in getString("key", "Guest")?

  • It sets the value if the key already exists
  • It deletes the key if no value is found
  • It is returned when the key does not exist yet
  • It prevents the app from crashing on null values

Files and Directories

What Are Files in Android?

Some data is too large or too complex for Shared Preferences — like saving a text document, downloading an image, or storing a log file. For this, Android lets you read and write actual files directly to the device's storage.

Think of it like the file manager on your phone. Just like you have folders for photos, downloads, and documents, Android apps can create and manage their own files and folders.

Two Types of Storage

Internal Storage

  • Private to your app — no other app or user can access it
  • Automatically deleted when the app is uninstalled
  • No permissions required

Location:

/data/data/com.yourapp.package/files/

External Storage

  • Accessible by the user and other apps (if permission is granted)
  • Remains even after uninstall (in some cases)
  • Requires runtime permissions: READ_EXTERNAL_STORAGE / WRITE_EXTERNAL_STORAGE

Location:

/sdcard/Android/data/com.yourapp.package/files/

Internal Storage Operations

Writing a File

val filename = "notes.txt"
val content = "This is my saved note."

// openFileOutput creates or opens the file
val fileOutputStream = openFileOutput(filename, MODE_PRIVATE)
fileOutputStream.write(content.toByteArray())
fileOutputStream.close()
  • MODE_PRIVATE — overwrites the file each time
  • MODE_APPEND — adds to the existing file instead of overwriting

Reading a File

val filename = "notes.txt"
val fileInputStream = openFileInput(filename)
val content = fileInputStream.bufferedReader().readText()
fileInputStream.close()

Deleting a File

deleteFile("notes.txt")

Working with Directories

You can create subdirectories inside your app's private storage:

// Get the app's internal files directory
val dir = File(filesDir, "my_folder")

// Create it if it doesn't exist
if (!dir.exists()) {
    dir.mkdir()
}

// Create a file inside the folder
val file = File(dir, "data.txt")
file.writeText("Hello from my_folder!")

Internal vs External Storage

Feature Internal Storage External Storage
Privacy App-private Publicly accessible
Permission needed No Yes (on older Android)
Deleted on uninstall Yes Not always
Best for Private logs, cache Downloads, shared media

MCQ

Which storage type is automatically deleted when the app is uninstalled and requires no permissions?

  • External Storage
  • Content Provider
  • Shared Preferences
  • Internal Storage

SQLite Database

What is SQLite?

You've stored simple key-value pairs with Shared Preferences. But what if your app needs to store hundreds of students, each with a name, age, and marks? What if you need to search, filter, or sort that data?

For structured data like this, Android uses SQLite — a lightweight, serverless relational database built directly into every Android device.

Think of SQLite like a mini version of MySQL running inside your phone — with tables, rows, columns, and SQL queries, but without needing any server.

  • Without SQLite: Storing 100 students in Shared Preferences = 300 separate keys = chaos
  • With SQLite: One table with 100 rows, easily searchable with a single query

Key Classes in Android SQLite

Class Role
SQLiteOpenHelper Creates and manages the database (your database manager)
SQLiteDatabase Lets you run queries (INSERT, SELECT, UPDATE, DELETE)
ContentValues Holds the data you want to insert or update (like a row)
Cursor Holds the results of a SELECT query (like a pointer to rows)

Step 1: Create a Database Helper Class

The SQLiteOpenHelper class handles creating and upgrading your database:

class DatabaseHelper(context: Context) :
    SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {

    companion object {
        const val DATABASE_NAME = "StudentDB"
        const val DATABASE_VERSION = 1
        const val TABLE_NAME = "students"
        const val COL_ID = "id"
        const val COL_NAME = "name"
        const val COL_MARKS = "marks"
    }

    // Called when the database is created for the first time
    override fun onCreate(db: SQLiteDatabase) {
        val createTable = """
            CREATE TABLE $TABLE_NAME (
                $COL_ID INTEGER PRIMARY KEY AUTOINCREMENT,
                $COL_NAME TEXT,
                $COL_MARKS INTEGER
            )
        """.trimIndent()
        db.execSQL(createTable)
    }

    // Called when the database version changes (for schema updates)
    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
        db.execSQL("DROP TABLE IF EXISTS $TABLE_NAME")
        onCreate(db)
    }
}
  • onCreate() runs only once — the very first time the database is created
  • onUpgrade() runs when you increase DATABASE_VERSION — useful when you need to add new columns later

Step 2: INSERT — Add a Record

fun addStudent(name: String, marks: Int): Long {
    val db = this.writableDatabase
    val values = ContentValues().apply {
        put(COL_NAME, name)
        put(COL_MARKS, marks)
    }
    val result = db.insert(TABLE_NAME, null, values)
    db.close()
    return result  // returns the row ID, or -1 if failed
}

ContentValues works like a map — you put column names and their values before inserting.

Step 3: SELECT — Read Records

fun getAllStudents(): List<String> {
    val studentList = mutableListOf<String>()
    val db = this.readableDatabase
    val cursor: Cursor = db.rawQuery("SELECT * FROM $TABLE_NAME", null)

    if (cursor.moveToFirst()) {
        do {
            val name = cursor.getString(cursor.getColumnIndexOrThrow(COL_NAME))
            val marks = cursor.getInt(cursor.getColumnIndexOrThrow(COL_MARKS))
            studentList.add("$name - $marks")
        } while (cursor.moveToNext())
    }
    cursor.close()
    db.close()
    return studentList
}

A Cursor works like a pointer that starts before the first row. You call moveToFirst() to go to row 1, then moveToNext() to advance row by row.

Step 4: UPDATE — Modify a Record

fun updateMarks(name: String, newMarks: Int): Int {
    val db = this.writableDatabase
    val values = ContentValues().apply {
        put(COL_MARKS, newMarks)
    }
    val rowsAffected = db.update(TABLE_NAME, values, "$COL_NAME=?", arrayOf(name))
    db.close()
    return rowsAffected
}

The ? is a placeholder — it prevents SQL injection by safely substituting the value.

Step 5: DELETE — Remove a Record

fun deleteStudent(name: String): Int {
    val db = this.writableDatabase
    val rowsDeleted = db.delete(TABLE_NAME, "$COL_NAME=?", arrayOf(name))
    db.close()
    return rowsDeleted
}

CRUD at a Glance

Operation Method Returns
Create db.insert(table, null, values) Row ID (Long)
Read db.rawQuery(sql, null) Cursor
Update db.update(table, values, where, args) Rows affected (Int)
Delete db.delete(table, where, args) Rows deleted (Int)

Using the Helper in an Activity

val db = DatabaseHelper(this)

// Insert
db.addStudent("Arjun", 88)

// Read
val students = db.getAllStudents()
students.forEach { println(it) }

// Update
db.updateMarks("Arjun", 95)

// Delete
db.deleteStudent("Arjun")

MCQ

What is the role of onUpgrade() in SQLiteOpenHelper?

  • It runs every time the app is launched
  • It creates the database for the first time
  • It is called when the database version is increased, allowing schema changes
  • It deletes all data from the database permanently

Content Providers

What Are Content Providers?

So far, every storage technique — Shared Preferences, Files, and SQLite — is private to your app. Other apps can't see your data and you can't see theirs.

But what if an app needs to share data? Your contacts app stores thousands of contacts. Your messaging app needs to access them. How does one app safely read another app's data?

This is exactly what Content Providers solve.

A Content Provider is Android's official way to share data between apps. It acts as a controlled gateway — exposing only what's needed, in a safe and structured way.

Think of it like a library:

  • Your app's SQLite database: Your private bookshelf at home
  • Content Provider: A public library — controlled access, anyone with the right request can borrow a book
  • Other apps: Library visitors who submit a request to the librarian

How Content Providers Work

Other App (Client)
   ↓ makes a request
ContentResolver
   ↓ routes the request using the URI
ContentProvider
   ↓ reads/writes
SQLite Database (or any data source)

The client app never directly touches the database. It goes through the ContentResolver, which contacts the ContentProvider using a special address called a Content URI.

Content URI

A Content URI is the unique address used to identify data inside a Content Provider. It works like a URL for your database.

Structure of a Content URI

content://authority/table_name/row_id
Part Meaning Example
content:// Fixed scheme — tells Android it's a Content Provider Always content://
authority Unique identifier of the provider (like a domain name) com.myapp.provider
table_name The data set or table being accessed students
row_id Optional — a specific row 5

Examples

content://com.myapp.provider/students         → All students
content://com.myapp.provider/students/3       → Student with ID 3

Defining the URI in Code

companion object {
    const val PROVIDER_NAME = "com.myapp.provider"
    const val URL = "content://$PROVIDER_NAME/students"
    val CONTENT_URI: Uri = Uri.parse(URL)
}

Content Resolver

The ContentResolver is the client-side bridge that your app uses to talk to any Content Provider. You never call the Content Provider directly — you always go through ContentResolver.

Every Activity has built-in access to it:

val resolver = contentResolver

CRUD Operations via ContentResolver

Query (Read)

val cursor: Cursor? = contentResolver.query(
    Uri.parse("content://com.myapp.provider/students"),  // URI
    null,          // projection (columns to return; null = all)
    null,          // selection (WHERE clause)
    null,          // selection args
    null           // sort order
)

cursor?.use {
    while (it.moveToNext()) {
        val name = it.getString(it.getColumnIndexOrThrow("name"))
        println(name)
    }
}

Insert

val values = ContentValues().apply {
    put("name", "Arjun")
    put("marks", 88)
}
val newUri = contentResolver.insert(
    Uri.parse("content://com.myapp.provider/students"),
    values
)

Update

val values = ContentValues().apply {
    put("marks", 95)
}
contentResolver.update(
    Uri.parse("content://com.myapp.provider/students/3"),
    values,
    null,
    null
)

Delete

contentResolver.delete(
    Uri.parse("content://com.myapp.provider/students/3"),
    null,
    null
)

Building Your Own Content Provider

To expose your app's data to other apps, extend the ContentProvider class and override its six methods:

class MyContentProvider : ContentProvider() {

    private lateinit var db: SQLiteDatabase

    // Called once when the provider is created
    override fun onCreate(): Boolean {
        db = DatabaseHelper(context!!).writableDatabase
        return true
    }

    override fun query(uri: Uri, projection: Array<String>?, selection: String?,
                       selectionArgs: Array<String>?, sortOrder: String?): Cursor? {
        return db.query("students", projection, selection, selectionArgs, null, null, sortOrder)
    }

    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        val id = db.insert("students", null, values)
        context?.contentResolver?.notifyChange(uri, null)
        return ContentUris.withAppendedId(CONTENT_URI, id)
    }

    override fun update(uri: Uri, values: ContentValues?, selection: String?,
                        selectionArgs: Array<String>?): Int {
        val count = db.update("students", values, selection, selectionArgs)
        context?.contentResolver?.notifyChange(uri, null)
        return count
    }

    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
        val count = db.delete("students", selection, selectionArgs)
        context?.contentResolver?.notifyChange(uri, null)
        return count
    }

    override fun getType(uri: Uri): String {
        return "vnd.android.cursor.dir/students"
    }
}

Register in AndroidManifest.xml

Every Content Provider must be declared in AndroidManifest.xml:

<provider
    android:name=".MyContentProvider"
    android:authorities="com.myapp.provider"
    android:exported="true" />
  • android:authorities — must match the authority in your Content URI exactly
  • android:exported="true" — allows other apps to access this provider

Built-in Content Providers

Android ships with several ready-made Content Providers that give apps access to system data. You don't build these — you just query them using ContentResolver.

Provider URI What It Gives Access To
Contacts content://com.android.contacts/contacts Phone contacts list
Call Logs content://call_log/calls Incoming/outgoing call history
SMS content://sms/ Text messages
Media Store content://media/external/images/media Photos, videos, audio files
Calendar content://com.android.calendar/events Calendar events
Browser Bookmarks content://browser/bookmarks Saved browser bookmarks

Example — Reading Contacts

val cursor = contentResolver.query(
    ContactsContract.Contacts.CONTENT_URI,
    null, null, null, null
)

cursor?.use {
    while (it.moveToNext()) {
        val name = it.getString(
            it.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME)
        )
        println(name)
    }
}

Add this permission in AndroidManifest.xml first:

<uses-permission android:name="android.permission.READ_CONTACTS" />

Content Provider vs Direct SQLite

Feature Direct SQLite Content Provider
Accessible by other apps No Yes
Security control App-level URI-level (fine-grained)
Used for In-app data only Shared data between apps
Requires permission? No Yes (if exported)

MCQ

What is the role of ContentResolver in Android?

  • It stores data in the SQLite database directly
  • It creates and manages Content Providers
  • It acts as a client-side bridge that routes requests to the appropriate Content Provider using a URI
  • It handles file read/write operations for internal storage