Android provides multiple mechanisms for persisting application data. This guide covers five fundamental storage techniques available on the Android platform, each suited for different use cases and data volumes.
- SharedPreferences
SharedPreferences offers a lightweight key-value storage solution ideal for configuration settings and user preferences. It stores data in XML files located at /data/data/<package_name>/shared_prefs/.
Unlike databases, SharedPreferences does not support complex queries or structured data. It handles only primitive types: boolean, int, float, long, and String. The storage mechanism uses an Editor pattern—retrieve the SharedPreferences instance, obtain a Editor, put key-value pairs, then commit.
Implementation example:
public class SettingsActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
SharedPreferences prefs = getSharedPreferences("user_settings", MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putString("username", "john_doe");
editor.putInt("age", 28);
editor.putBoolean("notifications_enabled", true);
editor.apply();
String storedName = prefs.getString("username", "default");
boolean notifications = prefs.getBoolean("notifications_enabled", false);
}
}
The apply() method commits changes asynchronously, while commit() writes synchronously and returns a boolean indicating success. SharedPreferences works well for small datasets but cannot replace databases for structured or queryable information.
- Internal File Storage
Activities provide openFileOutput() and openFileInput() methods for file-based storage. Files are stored in the app's private directory at /data/data/<package_name>/files/ by default.
Writing to internal storage:
public void saveUserData(String content) {
try {
FileOutputStream fos = openFileOutput("user_data.txt", MODE_PRIVATE);
fos.write(content.getBytes(StandardCharsets.UTF_8));
fos.close();
Toast.makeText(this, "File saved successfully", Toast.LENGTH_SHORT).show();
} catch (IOException e) {
e.printStackTrace();
}
}
Reading from internal storage:
public String loadUserData() {
StringBuilder buffer = new StringBuilder();
try {
FileInputStream fis = openFileInput("user_data.txt");
BufferedReader reader = new BufferedReader(new InputStreamReader(fis));
String line;
while ((line = reader.readLine()) != null) {
buffer.append(line);
}
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
return buffer.toString();
}
File Access Modes
Four access modes control file visibility:
- MODE_PRIVATE (0): Default mode, file accessible only by the creating application. New content overwrites existing data.
- MODE_APPEND (32768): Appends new content to existing files or creates new files if absent.
- MODE_WORLD_READABLE (1): Allows other applications to read the file.
- MODE_WORLD_WRITEABLE (2): Allows other applications to modify the file.
External Storage (SDCard)
Large files like images, videos, or cached data belong on external storage. Accessinng external storage requires permissions in AndroidManifest.xml:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
Writing to external storage:
public void saveToExternalStorage(String filename, String data) {
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
File externalDir = Environment.getExternalStorageDirectory();
File targetFile = new File(externalDir, filename);
try {
FileOutputStream fos = new FileOutputStream(targetFile);
fos.write(data.getBytes());
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Always verify storage availability using Environment.getExternalStorageState() before attempting write operations.
- SQLite Database Storage
SQLite provides a full relational database solution for structured data requiring queries, transactions, and relationships. Android includes SQLite as a native component, so no additional setup is needed.
Database Helper Implementation
Create a helper class extending SQLiteOpenHelper to manage database creation and version upgrades:
public class AppDatabase extends SQLiteOpenHelper {
private static final String DB_NAME = "app_database.db";
private static final int DB_VERSION = 1;
public AppDatabase(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
String createProductsTable = "CREATE TABLE products (" +
"id INTEGER PRIMARY KEY AUTOINCREMENT, " +
"name TEXT NOT NULL, " +
"price REAL, " +
"quantity INTEGER)";
db.execSQL(createProductsTable);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS products");
onCreate(db);
}
}
CRUD Operations
Obtain a database instance using getWritableDatabase() for modifications or getReadableDatabase() for read-only operations:
Inserting records:
public long addProduct(String name, double price, int quantity) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("name", name);
values.put("price", price);
values.put("quantity", quantity);
return db.insert("products", null, values);
}
Updating records:
public int updateProductQuantity(long productId, int newQuantity) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("quantity", newQuantity);
return db.update("products", values, "id = ?", new String[]{String.valueOf(productId)});
}
Deleting records:
public int deleteProduct(long productId) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
return db.delete("products", "id = ?", new String[]{String.valueOf(productId)});
}
Querying records:
public List<Product> getAllProducts() {
List<Product> productList = new ArrayList<>();
SQLiteDatabase db = dbHelper.getReadableDatabase();
String[] columns = {"id", "name", "price", "quantity"};
Cursor cursor = db.query("products", columns, null, null, null, null, "name ASC");
while (cursor.moveToNext()) {
Product product = new Product();
product.setId(cursor.getLong(0));
product.setName(cursor.getString(1));
product.setPrice(cursor.getDouble(2));
product.setQuantity(cursor.getInt(3));
productList.add(product);
}
cursor.close();
return productList;
}
SQLite supports flexible typing (values can be stored regardless of declared column types), which differs from strict SQL standards but provides convenience for mobile development.
- ContentProvider
ContentProviders enable data sharing between applications. Since Android sandboxes each app's private data, ContentProviders provide a standardized interface for inter-process communication.
Data is exposed through URIs following the format: content://authority/path/id
Using Existing ContentProviders
Query contacts:
public List<Contact> fetchContacts() {
List<Contact> contacts = new ArrayList<>();
ContentResolver resolver = getContentResolver();
String[] projection = {ContactsContract.Contacts._ID, ContactsContract.Contacts.DISPLAY_NAME};
Cursor cursor = resolver.query(
ContactsContract.Contacts.CONTENT_URI,
projection,
null,
null,
ContactsContract.Contacts.DISPLAY_NAME + " ASC"
);
if (cursor != null && cursor.moveToFirst()) {
do {
String id = cursor.getString(0);
String name = cursor.getString(1);
contacts.add(new Contact(id, name));
} while (cursor.moveToNext());
cursor.close();
}
return contacts;
}
Creating a Custom ContentProvider
Define the contract class first:
public class NotesContract {
public static final String AUTHORITY = "com.example.app.notes";
public static final Uri BASE_CONTENT_URI = Uri.parse("content://" + AUTHORITY);
public static final class Notes implements BaseColumns {
public static final Uri CONTENT_URI = BASE_CONTENT_URI.buildUpon()
.appendPath("notes").build();
public static final String TABLE_NAME = "notes";
public static final String COLUMN_TITLE = "title";
public static final String COLUMN_CONTENT = "content";
public static final String COLUMN_CREATED = "created_at";
}
}
Implement the ContentProvider:
public class NotesProvider extends ContentProvider {
private static final int NOTES = 1;
private static final int NOTE_ID = 2;
private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
sUriMatcher.addURI(NotesContract.AUTHORITY, "notes", NOTES);
sUriMatcher.addURI(NotesContract.AUTHORITY, "notes/#", NOTE_ID);
}
private DatabaseHelper dbHelper;
@Override
public boolean onCreate() {
dbHelper = new DatabaseHelper(getContext());
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SQLiteDatabase db = dbHelper.getReadableDatabase();
Cursor cursor;
switch (sUriMatcher.match(uri)) {
case NOTES:
cursor = db.query(NotesContract.Notes.TABLE_NAME,
projection, selection, selectionArgs, null, null, sortOrder);
break;
case NOTE_ID:
cursor = db.query(NotesContract.Notes.TABLE_NAME,
projection,
NotesContract.Notes._ID + " = ?",
new String[]{uri.getLastPathSegment()},
null, null, sortOrder);
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
if (sUriMatcher.match(uri) != NOTES) {
throw new IllegalArgumentException("Invalid URI for insert");
}
SQLiteDatabase db = dbHelper.getWritableDatabase();
long id = db.insert(NotesContract.Notes.TABLE_NAME, null, values);
if (id > 0) {
getContext().getContentResolver().notifyChange(uri, null);
return ContentUris.withAppendedId(uri, id);
}
throw new SQLException("Failed to insert row");
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
int rowsUpdated;
switch (sUriMatcher.match(uri)) {
case NOTES:
rowsUpdated = db.update(NotesContract.Notes.TABLE_NAME,
values, selection, selectionArgs);
break;
case NOTE_ID:
rowsUpdated = db.update(NotesContract.Notes.TABLE_NAME,
values,
NotesContract.Notes._ID + " = ?",
new String[]{uri.getLastPathSegment()});
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
if (rowsUpdated > 0) {
getContext().getContentResolver().notifyChange(uri, null);
}
return rowsUpdated;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
int rowsDeleted;
switch (sUriMatcher.match(uri)) {
case NOTES:
rowsDeleted = db.delete(NotesContract.Notes.TABLE_NAME,
selection, selectionArgs);
break;
case NOTE_ID:
rowsDeleted = db.delete(NotesContract.Notes.TABLE_NAME,
NotesContract.Notes._ID + " = ?",
new String[]{uri.getLastPathSegment()});
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
if (rowsDeleted > 0) {
getContext().getContentResolver().notifyChange(uri, null);
}
return rowsDeleted;
}
@Override
public String getType(Uri uri) {
switch (sUriMatcher.match(uri)) {
case NOTES:
return "vnd.android.cursor.dir/vnd." + NotesContract.AUTHORITY + ".notes";
case NOTE_ID:
return "vnd.android.cursor.item/vnd." + NotesContract.AUTHORITY + ".notes";
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
}
}
Register the ContentProvider in AndroidManifest.xml:
<provider
android:name=".NotesProvider"
android:authorities="com.example.app.notes"
android:exported="false" />
- Network Storage
Remote storage involves transmitting data to and from network servers. This approach suits scenarios where data must be synchronized across devices or when client-side storage is impractical.
Making HTTP requests with Apache HttpClient:
public class NetworkService {
private static final String API_ENDPOINT = "https://api.example.com/data";
public String fetchRemoteData(String param) throws IOException {
HttpClient client = new DefaultHttpClient();
HttpPost request = new HttpPost(API_ENDPOINT);
List<NameValuePair> parameters = new ArrayList<>();
parameters.add(new BasicNameValuePair("query", param));
request.setEntity(new UrlEncodedFormEntity(parameters, HTTP.UTF_8));
HttpResponse response = client.execute(request);
if (response.getStatusLine().getStatusCode() == 200) {
return EntityUtils.toString(response.getEntity());
}
throw new IOException("Request failed: " + response.getStatusLine().getStatusCode());
}
public String postWeatherRequest(String cityName) throws IOException {
HttpClient client = new DefaultHttpClient();
HttpPost postRequest = new HttpPost("http://www.webservicex.net/WeatherForecast.asmx/GetWeatherByPlaceName");
List<NameValuePair> params = new ArrayList<>();
params.add(new BasicNameValuePair("PlaceName", cityName));
postRequest.setEntity(new UrlEncodedFormEntity(params, HTTP.UTF_8));
HttpResponse response = client.execute(postRequest);
return EntityUtils.toString(response.getEntity());
}
}
Add the internet permission to AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
Network operations should be performed on background threads (using AsyncTask, Executors, or Kotlin Coroutines) to prevent blocking the main UI thread.
Summary
Each storage mechanism serves specific purposes: SharedPreferences for simple key-value pairs, internal files for unstructured data, SQLite for relational data requiring queries, ContentProviders for cross-application data sharing, and network storage for server-synchronized data. Selecting the appropriate approach depends on data structure, size, access patterns, and sharing requirements.