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, returnstrueorfalse
apply vs commit: Use
apply()for most cases since it doesn't block the main thread. Usecommit()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
EncryptedSharedPreferencesfrom 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 timeMODE_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 createdonUpgrade()runs when you increaseDATABASE_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 exactlyandroid: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
