Android Data Basics
Android Data Basics Interview with follow-up questions
1. Can you explain how SQLite is used in Android?
SQLite is a lightweight, file-based relational database engine embedded in Android. Every Android device includes SQLite, making it available without any external dependency. It is well-suited for structured local data that requires querying, sorting, and filtering.
Direct SQLite API (low-level, rarely used directly)
The raw API uses SQLiteOpenHelper to manage database creation and upgrades:
class AppDbHelper(context: Context) : SQLiteOpenHelper(context, "app.db", null, 2) {
override fun onCreate(db: SQLiteDatabase) {
db.execSQL("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)")
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL("ALTER TABLE users ADD COLUMN email TEXT")
}
}
You then query and insert using db.query(...), db.insert(...), etc. This approach is verbose, type-unsafe, and runs database operations on whichever thread you call it from (you must manage threading manually).
Room (recommended — Jetpack)
Room is the Jetpack abstraction over SQLite. It is the standard approach in modern Android development. Room provides compile-time verification of SQL queries, type-safe Kotlin APIs, coroutine and Flow support, and automated migration generation.
@Entity
data class User(
@PrimaryKey val id: Int,
val name: String,
val email: String
)
@Dao
interface UserDao {
@Query("SELECT * FROM user WHERE id = :id")
suspend fun getById(id: Int): User?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(user: User)
@Query("SELECT * FROM user ORDER BY name ASC")
fun observeAll(): Flow>
}
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
Key points about Room
@Entityannotates data classes that map to database tables.@Daoannotates interfaces with query methods. Room generates the implementation.suspendfunctions work directly with coroutines — Room runs them on an IO dispatcher automatically.Flowreturn types give you a reactive stream that emits new values whenever the underlying table changes — no manual re-querying needed.- SQL queries are validated at compile time; a typo in a table name causes a build error.
RoomDatabaseshould be injected as a singleton — Hilt provides@InstallIn(SingletonComponent::class)for this.
Schema migrations
When you increment the database version, Room requires you to provide a Migration object describing the schema change:
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE user ADD COLUMN email TEXT NOT NULL DEFAULT ''")
}
}
Room 2.4+ also supports auto-migrations for simple schema changes (adding/removing columns).
When to use SQLite/Room vs. alternatives
- Room: structured relational data, complex queries, need for reactive updates. The default choice.
- DataStore (Proto or Preferences): simple key-value settings or typed objects. Replaces SharedPreferences for new code.
- Files: binary blobs, media, large exports.
- Remote database (Firestore, Supabase): when data needs to sync across devices.
Common interview gotcha: never run database operations on the main thread. Room enforces this by default — calling a non-suspend DAO method on the main thread throws an IllegalStateException. Use suspend functions in a coroutine or Flow to observe changes reactively.
Follow-up 1
What are the advantages of using SQLite over other databases in Android?
There are several advantages of using SQLite over other databases in Android:
- Lightweight: SQLite is a lightweight database engine that is designed to be embedded in applications. It has a small footprint and minimal resource requirements, making it suitable for mobile devices with limited storage and processing power.
- Speed: SQLite is optimized for performance and can handle large amounts of data efficiently. It provides fast read and write operations, making it suitable for applications that require quick data access.
- Portability: SQLite databases are stored as files on the device's file system, making them easy to transfer and backup. They can be accessed and manipulated using standard SQL queries, which are supported by a wide range of programming languages and platforms.
- Compatibility: SQLite is included in the Android framework, so it is available on all Android devices. This ensures that your application can run on any Android device without requiring additional dependencies or installations.
Follow-up 2
How would you handle database version upgrades in SQLite?
In SQLite, database version upgrades can be handled by implementing the onUpgrade() method in the SQLiteOpenHelper class. This method is called when the database version is increased.
To handle a database version upgrade, you need to perform the following steps:
- Modify the database schema by adding, modifying, or deleting tables or columns.
- Increment the database version number in the
SQLiteOpenHelpersubclass. - Override the
onUpgrade()method and write the necessary code to upgrade the database.
In the onUpgrade() method, you can use SQL statements to perform the required database modifications, such as creating new tables, altering existing tables, or migrating data. It is important to handle the upgrade process carefully to avoid data loss or inconsistencies.
Follow-up 3
Can you explain the role of SQLiteOpenHelper in SQLite?
The SQLiteOpenHelper class is a helper class provided by the Android framework to manage SQLite databases. It simplifies the process of creating, opening, and upgrading databases.
The main role of the SQLiteOpenHelper class is to provide a set of methods that handle the creation and version management of the database. It provides two important methods:
onCreate(): This method is called when the database is created for the first time. It is used to create the database schema and initialize any necessary tables or data.onUpgrade(): This method is called when the database version is increased. It is used to handle database version upgrades by modifying the database schema or migrating data.
By subclassing SQLiteOpenHelper and implementing these methods, you can easily manage the lifecycle of your SQLite database in Android.
2. What is Parcelable in Android and why is it used?
Parcelable is an Android-specific interface for serializing objects so they can be passed between components via Intent extras, Bundles, or across process boundaries via AIDL. It is the standard way to pass custom data objects between Activities, Fragments, and Services on Android.
Why Parcelable instead of Java Serializable?
Java Serializable uses reflection to inspect the object graph at runtime, which is slow and creates a lot of temporary objects (putting pressure on the garbage collector). Parcelable requires you to explicitly write each field to a Parcel and read it back in the same order, which is much faster and creates fewer allocations. Benchmarks consistently show Parcelable to be roughly 10× faster than Serializable for the same object graph.
Implementing Parcelable
The traditional manual implementation is verbose. Modern Android uses the Kotlin Parcelize plugin, which generates all boilerplate at compile time:
// build.gradle.kts (app module)
plugins {
id("kotlin-parcelize")
}
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class User(
val id: Int,
val name: String,
val email: String
) : Parcelable
That's it — @Parcelize generates writeToParcel(), describeContents(), and the CREATOR factory, all from the data class properties. No boilerplate needed.
Passing Parcelable data
// Sending
val intent = Intent(this, DetailActivity::class.java).apply {
putExtra("user", user) // user is Parcelable
}
startActivity(intent)
// Receiving
val user: User? = intent.getParcelableExtra("user", User::class.java) // API 33+
// or on older APIs: intent.getParcelableExtra("user")
Parcelable in Fragments
// Sending via Bundle (Navigation Safe Args or manual)
val args = Bundle().apply { putParcelable("user", user) }
// Receiving
val user: User? = arguments?.getParcelable("user", User::class.java)
With Jetpack Navigation's Safe Args plugin, argument types are validated at compile time, including Parcelable types.
Common gotchas
- Order matters:
writeToParcel()andcreateFromParcel()must read and write fields in the same order.@Parcelizehandles this automatically. - Parcelable is not persistent storage: it is for in-memory IPC and Bundle passing. Don't use it to save data to disk — use Room or DataStore.
- Size limits: Binder (the IPC mechanism underlying Intents) has a ~1 MB buffer limit across all concurrent transactions. Avoid passing large bitmaps or collections via Parcelable; pass identifiers and load data from a repository instead.
- API 33
getParcelableExtra(String, Class): the oldgetParcelableExtra(String)is deprecated on API 33+ and generates unchecked cast warnings. Use the class-typed version on API 33+ with aBuild.VERSION.SDK_INTcheck or use theBundleCompat/IntentCompatcompat classes fromandroidx.core.
Follow-up 1
How does Parcelable compare to Serializable?
Parcelable and Serializable are both interfaces in Java that allow objects to be serialized and deserialized. However, Parcelable is specifically designed for Android and is optimized for performance. Here are some differences between Parcelable and Serializable:
- Parcelable is faster and more efficient than Serializable.
- Parcelable requires you to implement the
writeToParcel()andcreateFromParcel()methods, while Serializable uses Java's default serialization mechanism. - Parcelable is more complex to implement compared to Serializable.
In general, if you are passing data between components within an Android application, it is recommended to use Parcelable for better performance.
Follow-up 2
Can you explain the process of making a class Parcelable?
To make a class Parcelable in Android, you need to follow these steps:
- Implement the
Parcelableinterface in your class. - Override the
writeToParcel()method to write the object's data to aParcelobject. - Implement the
Parcelable.Creatorinterface and create aCREATORfield. - Override the
createFromParcel()method to read the object's data from aParcelobject. - Implement the
describeContents()method and return 0.
Here is an example of how to make a class Parcelable:
public class MyParcelable implements Parcelable {
private int data;
public MyParcelable(int data) {
this.data = data;
}
protected MyParcelable(Parcel in) {
data = in.readInt();
}
public static final Creator CREATOR = new Creator() {
@Override
public MyParcelable createFromParcel(Parcel in) {
return new MyParcelable(in);
}
@Override
public MyParcelable[] newArray(int size) {
return new MyParcelable[size];
}
};
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(data);
}
@Override
public int describeContents() {
return 0;
}
}
Follow-up 3
What are some potential issues you might encounter when using Parcelable?
When using Parcelable in Android, there are a few potential issues you might encounter:
- Versioning: If you modify the structure of a Parcelable class, you need to ensure backward compatibility with older versions of the class.
- Complex objects: Parcelable is more complex to implement compared to Serializable, especially for classes with complex object hierarchies or circular references.
- Performance trade-offs: While Parcelable is faster and more efficient than Serializable, it requires more code to implement. Depending on the specific use case, the performance gain may or may not be significant.
It's important to carefully consider these factors and choose the appropriate serialization mechanism based on your specific requirements.
3. How do you use SharedPreferences in Android?
SharedPreferences is Android's legacy key-value store for persisting small amounts of primitive data (strings, ints, booleans, floats, long, sets of strings). It persists across app sessions and process deaths. For new code, Jetpack DataStore is the recommended replacement — it is asynchronous, safe on the main thread, and does not have SharedPreferences' data consistency issues. That said, SharedPreferences is still widely used and appears in many existing codebases.
Basic usage (SharedPreferences)
// Writing
val prefs = getSharedPreferences("settings", Context.MODE_PRIVATE)
prefs.edit()
.putString("username", "alice")
.putBoolean("notifications_enabled", true)
.apply() // async commit; use .commit() only if you need a synchronous return value
// Reading
val username = prefs.getString("username", "guest") // "guest" is the default
val notificationsEnabled = prefs.getBoolean("notifications_enabled", true)
// Observing changes
prefs.registerOnSharedPreferenceChangeListener { _, key ->
if (key == "username") { /* handle change */ }
}
apply() vs commit()
apply(): writes to the in-memory copy immediately, persists to disk asynchronously. Returnsvoid. Safe to call on the main thread.commit(): writes synchronously to disk, returnstrueon success. Blocks the calling thread — avoid on the main thread.
Prefer DataStore for new code
Jetpack DataStore (in androidx.datastore) offers two variants:
Preferences DataStore (drop-in replacement for SharedPreferences):
// Define
val Context.dataStore: DataStore by preferencesDataStore(name = "settings")
val USERNAME_KEY = stringPreferencesKey("username")
// Write (suspending, runs on IO dispatcher internally)
context.dataStore.edit { prefs -> prefs[USERNAME_KEY] = "alice" }
// Read (returns Flow — observe reactively)
val usernameFlow: Flow = context.dataStore.data.map { it[USERNAME_KEY] ?: "guest" }
Proto DataStore: stores typed objects defined with Protocol Buffers. Provides full type safety and schema evolution, but requires a .proto schema file.
Why DataStore over SharedPreferences
- SharedPreferences performs disk I/O on the main thread when you call
getSharedPreferences()the first time (it reads the entire XML file). This can cause ANR (Application Not Responding) under load. - SharedPreferences
apply()can cause StrictMode violations and occasionally data corruption on process crashes. - DataStore is built on coroutines and Flows — reads and writes are non-blocking and composable.
- DataStore is scoped to a coroutine scope and integrates cleanly with the ViewModel lifecycle.
When to still use SharedPreferences
- Maintaining an existing codebase that already uses SharedPreferences widely.
- Third-party libraries that expose SharedPreferences-based APIs.
- Very simple, one-off preference reads during initialization where DataStore's async nature adds complexity without benefit.
Common gotcha: SharedPreferences stores data as an XML file in the app's private directory. The file is loaded entirely into memory when first accessed — storing large datasets in SharedPreferences (e.g., a serialized list) causes performance problems. Use Room for structured data and files or DataStore Proto for typed complex objects.
Follow-up 1
What are some use cases for SharedPreferences?
SharedPreferences is commonly used for storing small amounts of data that needs to persist across app sessions. Some common use cases for SharedPreferences include:
- Storing user preferences and settings
- Storing user authentication tokens
- Storing app configuration settings
- Storing user interface state (e.g., selected tab, expanded/collapsed views)
- Storing user-selected language or theme preferences
It's important to note that SharedPreferences should not be used for storing large amounts of data or sensitive information.
Follow-up 2
What are the limitations of SharedPreferences?
SharedPreferences has some limitations that you should be aware of:
- It can only store primitive data types (e.g., boolean, int, float, long, String)
- It is not designed for storing large amounts of data
- It is not secure and should not be used for storing sensitive information
- It is not suitable for multi-process applications
If you need to store complex data structures or larger amounts of data, you should consider using other storage mechanisms such as a SQLite database or file storage.
Follow-up 3
How would you secure data stored in SharedPreferences?
SharedPreferences is not secure by default, so if you need to store sensitive information, you should take additional steps to secure the data. Here are some approaches you can consider:
- Encrypt the data before storing it in SharedPreferences and decrypt it when retrieving it
- Use Android's Keystore system to securely store encryption keys
- Use a custom encryption algorithm or library to encrypt the data
- Implement additional security measures such as obfuscation or code obfuscation to make it harder for attackers to reverse engineer the app
It's important to note that even with these measures, SharedPreferences may not provide the same level of security as other storage mechanisms designed specifically for sensitive data.
4. What are content providers in Android?
Content Providers are one of Android's four core application components (alongside Activities, Services, and BroadcastReceivers). They expose structured data from one application to other applications or system components through a standardized URI-based interface.
What they do
A Content Provider sits in front of a data source (typically a Room/SQLite database, but also files or network data) and exposes it via URIs of the form content://authority/path/id. Other apps (and system components) query, insert, update, and delete data through a ContentResolver, which routes the call to the appropriate provider process.
How they work
// Querying a Content Provider (consumer side)
val cursor = contentResolver.query(
Uri.parse("content://com.example.myapp.provider/users"),
arrayOf("_id", "name", "email"), // projection (columns)
"name = ?", // selection
arrayOf("Alice"), // selection args
"name ASC" // sort order
)
cursor?.use {
while (it.moveToNext()) {
val name = it.getString(it.getColumnIndexOrThrow("name"))
}
}
Implementing a Content Provider
You extend ContentProvider and override query(), insert(), update(), delete(), and getType(). You declare it in the manifest with an `element and setandroid:exported` to control visibility.
When are Content Providers actually needed?
This is the key question interviewers probe. In modern Android development, you rarely write a Content Provider for internal use within your own app:
- Internal data sharing: use Room, DataStore, or a Singleton repository accessed via Hilt — these are simpler and faster.
- Exposing data to other apps: Content Provider is the correct tool. Example: an app that exposes contact data, media files, or custom data sets for other apps to read.
- System integration: required for specific system features —
CameraXandFileProviderfor sharing files safely,SyncAdapterfor background syncing with an account,SearchSuggestionProviderfor system-wide search suggestions, andAutofillServicefor exposing fillable fields.
FileProvider (most commonly encountered in 2026)
FileProvider is a concrete ContentProvider subclass from androidx.core that generates content:// URIs for files in your app's private storage. It is the standard, required way to share files with other apps (camera apps, email clients, etc.) since Android 7 Nougat, which blocked file:// URIs in Intents:
val uri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
file
)
intent.setDataAndType(uri, "application/pdf")
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
Permissions and security
Content Providers enforce access control via:
android:permission/android:readPermission/android:writePermissionon the `` element.Intent.FLAG_GRANT_READ_URI_PERMISSIONfor temporary, single-use URI grants to specific apps (used with FileProvider).android:exported="false"to prevent other apps from accessing it at all.
Common gotcha: Content Providers are initialized before Application.onCreate() is called, making them an (ab)used vector for eager library initialization (the startup library's InitializationProvider does this). Be aware that a misbehaving third-party Content Provider can slow your app startup — the App Startup library lets you control initialization order and laziness.
Follow-up 1
How do content providers differ from direct database access?
Content providers provide an abstraction layer over the underlying data source, which can be a SQLite database or any other data storage. They allow other applications to access and modify the data through a well-defined interface, while enforcing data security and permission checks. On the other hand, direct database access involves accessing the database directly using the SQLite APIs, which can be more efficient for simple operations but lacks the security and permission controls provided by content providers.
Follow-up 2
Can you explain how to use a content provider?
To use a content provider in Android, you need to follow these steps:
- Define a contract class that specifies the data columns and URIs used by the content provider.
- Implement a subclass of
ContentProviderand override its methods to handle data queries, insertions, updates, and deletions. - Register the content provider in the AndroidManifest.xml file.
- Use the
ContentResolverclass to interact with the content provider from other components of your application or from other applications.
Here's an example of how to query data from a content provider:
// Define the URI and projection for the data you want to query
Uri uri = Uri.parse("content://com.example.provider/data");
String[] projection = {"column1", "column2"};
// Use the ContentResolver to query the data
Cursor cursor = getContentResolver().query(uri, projection, null, null, null);
// Iterate over the cursor to retrieve the data
if (cursor != null) {
while (cursor.moveToNext()) {
String column1Value = cursor.getString(cursor.getColumnIndex("column1"));
String column2Value = cursor.getString(cursor.getColumnIndex("column2"));
// Do something with the data
}
cursor.close();
}
Follow-up 3
What are some common use cases for content providers in Android?
Some common use cases for content providers in Android include:
- Sharing data between different applications: Content providers allow different applications to access and modify the same data, enabling data sharing and collaboration.
- Exposing data to other applications: Content providers can be used to expose data from your application to other applications, such as providing access to a database of contacts or a collection of media files.
- Content synchronization: Content providers can be used to synchronize data between a local database and a remote server, ensuring that the data is always up to date.
- Content search: Content providers can be used to implement search functionality, allowing users to search for specific data within your application.
These are just a few examples, and the use of content providers can vary depending on the specific requirements of your application.
5. What are loaders in Android and why are they used?
Loaders were introduced in Android 3.0 (Honeycomb) to solve the problem of loading data asynchronously in a way that survived configuration changes (like screen rotation). They are now deprecated as of Android 9 (API 28) and the deprecation was further emphasized in subsequent Jetpack releases. You should not use Loaders in new code.
What Loaders did
The LoaderManager and CursorLoader / AsyncTaskLoader APIs allowed you to:
- Load data in a background thread.
- Deliver results to the UI thread.
- Survive Activity/Fragment recreation (configuration changes) without restarting the load.
- Automatically restart the load when the underlying data changed (for
CursorLoaderbacked by aContentProvider).
Why they are deprecated
The same problems Loaders solved are now solved better by the Jetpack ViewModel + LiveData/Flow pattern, which is cleaner, more testable, and composable:
| Problem | Old solution | Modern solution |
|---|---|---|
| Survive configuration changes | LoaderManager |
ViewModel |
| Background data loading | AsyncTaskLoader |
Kotlin coroutines (viewModelScope.launch) |
| Reactive data updates | CursorLoader with ContentProvider |
Room returning Flow |
| Lifecycle-aware delivery | Loader callbacks | collectAsStateWithLifecycle() / LiveData.observe(viewLifecycleOwner) |
The modern replacement pattern
// ViewModel
class UsersViewModel(private val repo: UserRepository) : ViewModel() {
val users: StateFlow> = repo.observeUsers()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
}
// Repository
class UserRepository(private val dao: UserDao) {
fun observeUsers(): Flow> = dao.observeAll() // Room Flow
}
// Fragment / Activity (Compose)
@Composable
fun UsersScreen(viewModel: UsersViewModel = hiltViewModel()) {
val users by viewModel.users.collectAsStateWithLifecycle()
LazyColumn {
items(users, key = { it.id }) { user -> UserItem(user) }
}
}
This pattern:
- Survives configuration changes because the ViewModel is retained.
- Loads data off the main thread (Room dispatches Flow emissions to an IO dispatcher).
- Delivers results lifecycle-safely (
collectAsStateWithLifecyclestops collecting when the lifecycle is stopped). - Reacts to database changes automatically via Room's
Flow.
When you might still encounter Loaders
In legacy codebases (pre-2019) that use CursorLoader with Content Providers, or in apps targeting older API levels that have not been modernized. Migration is straightforward: replace LoaderManager callbacks with a ViewModel that collects a Flow from Room.
Note on AsyncTask: AsyncTask was the other common background-threading mechanism from the same era. It was deprecated in Android 11 (API 30). Both AsyncTask and Loaders should be replaced with Kotlin coroutines in any modernization effort.
Follow-up 1
How do loaders handle data?
Loaders handle data by performing the data loading and processing tasks in a separate background thread. They use the LoaderManager to manage the lifecycle of the loader and handle configuration changes. Loaders can be used to load data from various sources such as databases, content providers, or network requests. Once the data is loaded, the loader delivers the result to the UI thread, where it can be displayed or used by the application.
Follow-up 2
What are the advantages of using loaders?
There are several advantages of using loaders in Android:
Asynchronous Loading: Loaders perform data loading tasks in the background, allowing the UI thread to remain responsive and preventing the application from freezing or becoming unresponsive.
Lifecycle Management: Loaders are integrated with the LoaderManager, which handles the lifecycle of the loader and automatically manages configuration changes. This simplifies the management of data loading and ensures that the data is retained across configuration changes.
Data Caching: Loaders have built-in support for caching data, which helps in improving performance and reducing the need for repeated data loading.
Content Observer Integration: Loaders can be used in conjunction with ContentObservers to automatically update the UI when the underlying data changes.
Multiple Loader Support: Android supports multiple loaders, allowing parallel loading of data from different sources.
Follow-up 3
Can you explain the lifecycle of a loader?
The lifecycle of a loader in Android consists of the following stages:
Creation: A loader is created using the LoaderManager's
initLoader()orrestartLoader()methods. The loader is associated with a unique ID and a LoaderCallbacks object.Initialization: The loader's
onCreateLoader()method is called, where the data loading task is defined. This method returns a Loader object that will be used to load the data.Loading: The loader's
onLoadInBackground()method is called in a background thread. This is where the actual data loading task is performed. The result of the loading task is returned by this method.Delivery: The result of the loading task is delivered to the UI thread by calling the loader's
onLoadFinished()method. This method is called on the LoaderCallbacks object associated with the loader.Reset: The loader's
onReset()method is called when the loader is no longer needed or when the activity or fragment is destroyed. This method allows the loader to release any resources it holds and clean up its state.
Live mock interview
Mock interview: Android Data Basics
- Read your scene and goals
- Talk it out; goals tick off live
- Get a score and stronger lines
Your voice and your AI key never touch our servers; the key stays in this browser and is sent only to Google. Only your round scores are saved to track progress.