Five Data Storage Approaches for Android Applications

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.

  1. 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.

  1. 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.

  1. 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.

  1. 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" />
  1. 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.

Tags: Android sharedpreferences sqlite contentprovider file-storage

Posted on Mon, 06 Jul 2026 16:44:55 +0000 by ctiberg