Topic: 13 Understanding Data Storage in Android

Mayursinh ParmarMayursinh Parmar
15 min read

Hello devs, Today's topic is Data storage in Android. Every time we didn't store data on a server. Sometimes we need to store the data Locally. There are several options for data storage depending on the requirements of your application like storing in shared preference, Internal Storage, External Storage, and many more. Let's Explore one by one.

Shared Preferences

Shared Preferences are used for storing small pieces of data. It allows you to save key-value pairs in a file that persevere even if the application is closed. This makes it ideal for storing simple user preferences, settings, and other lightweight data.

Imagine you're building a notes app and you want to allow users to choose their preferred theme color. Shared Preferences would be perfect for storing this user preference.

  1. Initialization:
val sharedPreferences = getSharedPreferences("MyAppPreferences", Context.MODE_PRIVATE)
  1. Saving Data:
val editor = sharedPreferences.edit()
editor.putString("theme_color", "#FF5722") // Assuming the theme color is orange
editor.apply()
  1. Retrieving Data:
val themeColor = sharedPreferences.getString("theme_color", "#000000") // Default color is black

In this Kotlin version:

  • getSharedPreferences() retrieves the SharedPreferences object just like in Java.

  • edit() method creates a SharedPreferences.Editor instance for making modifications.

  • putString() method stores the chosen theme color with the key "theme_color".

  • apply() method commits the changes asynchronously.

  • When retrieving, getString() method retrieves the theme color from SharedPreferences. If the key doesn't exist, it returns the default value "#000000" (black).

Internal Storage

Internal Storage is a way to store private data on the device's internal memory. This storage is private to your application and other apps cannot access it. It's suitable for storing sensitive information or data that only your app needs to access.

Here's a detailed explanation along with a short Kotlin example:

  1. Understanding Internal Storage:

    Internal Storage provides a private filesystem for your app. Files stored in internal storage are accessible only to your app and cannot be accessed by other apps or users. This makes it ideal for storing sensitive user data like login credentials, cached files, or any other private data your app needs.

  2. Example Usage:

    Let's say you're developing a notes app and you want to save the user's notes locally on the device's internal storage.

// Function to save note to internal storage
fun saveNoteToInternalStorage(note: String) {
    try {
        // Open a file named "notes.txt" in private mode
        val fileOutputStream = openFileOutput("notes.txt", Context.MODE_PRIVATE)

        // Write the note string to the file
        fileOutputStream.write(note.toByteArray())

        // Close the file output stream
        fileOutputStream.close()
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

// Function to read note from internal storage
fun readNoteFromInternalStorage(): String {
    var note = ""
    try {
        // Open the file named "notes.txt"
        val fileInputStream = openFileInput("notes.txt")

        // Read the contents of the file into a ByteArray
        val byteArray = ByteArray(fileInputStream.available())
        fileInputStream.read(byteArray)

        // Convert the ByteArray to a String
        note = String(byteArray)

        // Close the file input stream
        fileInputStream.close()
    } catch (e: Exception) {
        e.printStackTrace()
    }
    return note
}

In this example:

  • openFileOutput() method creates a FileOutputStream to write data to a file named "notes.txt" in the app's private internal storage. We use MODE_PRIVATE to ensure that the file is accessible only to our app.

  • write() method writes the note string to the file.

  • openFileInput() method creates a FileInputStream to read data from the "notes.txt" file.

  • read() method reads the contents of the file into a ByteArray.

  • We then convert the ByteArray to a String and return the note.

By using Internal Storage, your app can securely store sensitive user data without exposing it to other apps or users. However, remember that Internal Storage has limited space, so it's best suited for small to moderate amounts of data. If your app needs to store larger amounts of data, you might consider using External Storage or a local database instead.

External Storage

External Storage in Android refers to the storage that's accessible to both the user and other apps. It includes the device's shared external storage (such as an SD card) that can be removed by the user. This storage is suitable for storing large files that need to be shared across different applications or accessible to the user directly.

Here's a detailed explanation along with a short Kotlin example:

  1. Understanding External Storage:

    External Storage provides a shared filesystem that can be accessed by both your app and other apps on the device. It's typically used for storing files that need to be shared between apps or accessed by the user, such as photos, videos, or documents. Unlike Internal Storage, External Storage is accessible to other apps and users, so it's important to be mindful of security and permissions when using it.

  2. Example Usage:

    Let's say you're building a photo-sharing app and you want to allow users to save photos to their device's external storage.

// Function to save photo to external storage
fun savePhotoToExternalStorage(context: Context, photoBitmap: Bitmap): Boolean {
    return try {
        // Get the external storage directory
        val externalStorageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)

        // Create a file named "my_photo.jpg" in the Pictures directory
        val file = File(externalStorageDir, "my_photo.jpg")

        // Create a FileOutputStream to write data to the file
        val fileOutputStream = FileOutputStream(file)

        // Compress the bitmap to JPEG format and write it to the FileOutputStream
        photoBitmap.compress(Bitmap.CompressFormat.JPEG, 100, fileOutputStream)

        // Close the FileOutputStream
        fileOutputStream.close()

        // Return true indicating success
        true
    } catch (e: Exception) {
        e.printStackTrace()
        // Return false indicating failure
        false
    }
}

// Function to check if external storage is available for read and write operations
fun isExternalStorageWritable(): Boolean {
    return Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED
}

In this example:

  • Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) retrieves the directory where pictures should be stored on the external storage.

  • We create a file named "my_photo.jpg" in the Pictures directory.

  • We create a FileOutputStream to write data to the file and then compress the Bitmap image to JPEG format and write it to the FileOutputStream.

  • isExternalStorageWritable() checks if the external storage is available for read and write operations by checking the state of the external storage.

Using External Storage allows your app to store large files that need to be shared between apps or accessed by the user. However, it's important to handle permissions properly and be aware that external storage may not always be available (e.g., if the user removes the SD card). Additionally, be mindful of the limited space on external storage and avoid storing large amounts of data unnecessarily.

SQLite Database

SQLite Database is a lightweight, embedded relational database management system that is widely used in Android app development for storing structured data. It provides a powerful and efficient way to manage data locally within an Android application. SQLite databases are self-contained, meaning they require minimal configuration and can be easily included with an Android app without requiring a separate server or setup.

Here's a detailed explanation along with a short Kotlin example:

  1. Understanding SQLite Database:

    SQLite is a relational database management system that is implemented as a C library. In Android development, SQLite is used to store structured data in a local database file that resides on the device. It supports standard SQL syntax and provides features like transactions, indexes, and triggers.

  2. Example Usage:

    Let's say you're developing a simple task management app and you want to store tasks in a SQLite database.

import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper

// Define the database schema
object TaskContract {
    const val TABLE_NAME = "tasks"
    const val COLUMN_ID = "_id"
    const val COLUMN_TASK_NAME = "task_name"
}

// Create a helper class to manage database creation and version management
class TaskDBHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {

    companion object {
        const val DATABASE_NAME = "tasks.db"
        const val DATABASE_VERSION = 1
    }

    override fun onCreate(db: SQLiteDatabase?) {
        // Create the tasks table
        db?.execSQL(
            "CREATE TABLE ${TaskContract.TABLE_NAME} (" +
                    "${TaskContract.COLUMN_ID} INTEGER PRIMARY KEY AUTOINCREMENT," +
                    "${TaskContract.COLUMN_TASK_NAME} TEXT NOT NULL)"
        )
    }

    override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
        // Drop the tasks table if it exists
        db?.execSQL("DROP TABLE IF EXISTS ${TaskContract.TABLE_NAME}")
        onCreate(db)
    }
}

// Function to add a new task to the database
fun addTask(context: Context, taskName: String): Long {
    val dbHelper = TaskDBHelper(context)
    val db = dbHelper.writableDatabase

    // Create a ContentValues object to store the task data
    val values = ContentValues().apply {
        put(TaskContract.COLUMN_TASK_NAME, taskName)
    }

    // Insert the new task into the database
    val newRowId = db.insert(TaskContract.TABLE_NAME, null, values)

    // Close the database connection
    db.close()

    return newRowId
}

// Function to retrieve all tasks from the database
fun getAllTasks(context: Context): ArrayList<String> {
    val dbHelper = TaskDBHelper(context)
    val db = dbHelper.readableDatabase
    val cursor = db.query(
        TaskContract.TABLE_NAME,
        arrayOf(TaskContract.COLUMN_TASK_NAME),
        null,
        null,
        null,
        null,
        null
    )

    val tasks = ArrayList<String>()

    // Iterate over the cursor and add tasks to the list
    while (cursor.moveToNext()) {
        val taskName = cursor.getString(cursor.getColumnIndex(TaskContract.COLUMN_TASK_NAME))
        tasks.add(taskName)
    }

    // Close the cursor and database connection
    cursor.close()
    db.close()

    return tasks
}

In this example:

  • We define a contract class TaskContract to define the table and column names.

  • We create a helper class TaskDBHelper that extends SQLiteOpenHelper to manage database creation and version management.

  • In the onCreate() method of TaskDBHelper, we define the SQL statement to create the tasks table.

  • We implement functions addTask() and getAllTasks() to insert new tasks into the database and retrieve all tasks from the database, respectively.

  • Inside these functions, we obtain a writable or readable database instance using writableDatabase or readableDatabase methods, respectively.

  • We use ContentValues to insert data into the database and Cursor to retrieve data from the database.

Using SQLite Database allows your app to store and manage structured data efficiently. It's well-suited for scenarios where you need to store relational data or large datasets locally within your app. However, for more complex data management requirements or scenarios involving concurrent access from multiple threads, you may need to consider using other persistence solutions like Room Persistence Library or Firebase Realtime Database.

Room Database

Room is a persistence library provided by Google as part of the Android Jetpack suite. It's built on top of the SQLite database and provides an abstraction layer over raw SQLite queries. Room simplifies database interactions by providing compile-time checks for SQL queries and LiveData support for observing database changes. It's designed to be efficient, type-safe, and easy to use, making it a preferred choice for managing app data locally on Android.

Here's a detailed explanation along with a short Kotlin example:

  1. Understanding Room Database:

    The room consists of three main components: entities, DAOs (Data Access Objects), and the database itself.

    • Entity: Annotated Kotlin data class representing a table in the database.

    • DAO (Data Access Object): Interface containing methods for interacting with the database. Each method corresponds to a database operation (e.g., insert, update, delete).

    • Database: Abstract class extending RoomDatabase that defines the database and its version and provides access to DAOs.

  2. Example Usage:

    Let's continue with the task management app example and refactor it to use Room for managing tasks.

import androidx.room.*

// Define the Task entity
@Entity(tableName = "tasks")
data class Task(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,
    @ColumnInfo(name = "task_name")
    val taskName: String
)

// Define the TaskDAO interface
@Dao
interface TaskDao {
    @Insert
    fun insertTask(task: Task)

    @Query("SELECT * FROM tasks")
    fun getAllTasks(): List<Task>
}

// Define the TaskDatabase abstract class
@Database(entities = [Task::class], version = 1)
abstract class TaskDatabase : RoomDatabase() {
    abstract fun taskDao(): TaskDao
}

In this example:

  • We define a Task entity class using annotations provided by Room. The @Entity annotation specifies the table name, and the @PrimaryKey annotation specifies the primary key column.

  • We define a TaskDao interface containing methods for database operations. We use annotations like @Insert and @Query to define insert and select queries, respectively.

  • We define a TaskDatabase abstract class extending RoomDatabase. We annotate it with @Database specifying the entities and the database version.

  • Room automatically generates the implementation for TaskDao at compile time.

Now, let's use Room in our app to insert and retrieve tasks:

// Function to add a new task to the database using Room
fun addTask(context: Context, taskName: String) {
    val task = Task(taskName = taskName)
    val db = Room.databaseBuilder(context, TaskDatabase::class.java, "task-db").build()
    db.taskDao().insertTask(task)
}

// Function to retrieve all tasks from the database using Room
fun getAllTasks(context: Context): List<Task> {
    val db = Room.databaseBuilder(context, TaskDatabase::class.java, "task-db").build()
    return db.taskDao().getAllTasks()
}

In this example, we use Room's database builder to create an instance of the database (TaskDatabase). We then use the DAO (TaskDao) to perform database operations like insertion and retrieval of tasks.

Using Room Database simplifies database operations by providing compile-time checks and reducing boilerplate code. It also offers support for LiveData, which makes it easy to observe changes in the database and update UI accordingly. Room is a powerful and efficient persistence library that helps in building robust and maintainable Android applications.

Network storage

Network storage, in the context of Android development, refers to the storage of data on remote servers or cloud-based storage solutions accessible over a network, typically the Internet. This enables apps to store and retrieve data from remote locations, providing scalability, accessibility, and data persistence across multiple devices.

Here's a detailed explanation along with a short Kotlin example:

  • Understanding Network Storage:

    Network storage solutions allow Android apps to store data remotely, enabling seamless access to data across different devices and platforms. This includes various cloud storage providers such as Google Drive, Dropbox, Amazon S3, Firebase Cloud Storage, and custom server-based solutions. By leveraging network storage, apps can store large amounts of data, such as user files, media assets, user preferences, and application state, without relying solely on local device storage.

  • Example Usage:

    Let's consider an example where you're developing a file-sharing app that allows users to upload and download files from a cloud storage service like Firebase Cloud Storage.

      import com.google.firebase.ktx.Firebase
      import com.google.firebase.storage.ktx.storage
    
      // Function to upload a file to Firebase Cloud Storage
      fun uploadFileToFirebaseStorage(context: Context, fileUri: Uri, fileName: String) {
          val storageRef = Firebase.storage.reference
          val fileReference = storageRef.child("uploads/$fileName")
    
          val uploadTask = fileReference.putFile(fileUri)
    
          uploadTask.addOnSuccessListener {
              // File uploaded successfully
              Toast.makeText(context, "File uploaded successfully", Toast.LENGTH_SHORT).show()
          }.addOnFailureListener { exception ->
              // Handle errors
              Toast.makeText(context, "Error uploading file: ${exception.message}", Toast.LENGTH_SHORT).show()
          }
      }
    
      // Function to download a file from Firebase Cloud Storage
      fun downloadFileFromFirebaseStorage(context: Context, fileName: String, localFile: File) {
          val storageRef = Firebase.storage.reference
          val fileReference = storageRef.child("uploads/$fileName")
    
          val downloadTask = fileReference.getFile(localFile)
    
          downloadTask.addOnSuccessListener {
              // File downloaded successfully
              Toast.makeText(context, "File downloaded successfully", Toast.LENGTH_SHORT).show()
          }.addOnFailureListener { exception ->
              // Handle errors
              Toast.makeText(context, "Error downloading file: ${exception.message}", Toast.LENGTH_SHORT).show()
          }
      }
    

    Network storage, in the context of Android development, refers to the storage of data on remote servers or cloud-based storage solutions accessible over a network, typically the internet. This enables apps to store and retrieve data from remote locations, providing scalability, accessibility, and data persistence across multiple devices.

    Here's a detailed explanation along with a short Kotlin example:

    1. Understanding Network Storage:

      Network storage solutions allow Android apps to store data remotely, enabling seamless access to data across different devices and platforms. This includes various cloud storage providers such as Google Drive, Dropbox, Amazon S3, Firebase Cloud Storage, and custom server-based solutions. By leveraging network storage, apps can store large amounts of data, such as user files, media assets, user preferences, and application state, without relying solely on local device storage.

    2. Example Usage:

      Let's consider an example where you're developing a file-sharing app that allows users to upload and download files from a cloud storage service like Firebase Cloud Storage.

    kotlinCopy codeimport com.google.firebase.ktx.Firebase
    import com.google.firebase.storage.ktx.storage

    // Function to upload a file to Firebase Cloud Storage
    fun uploadFileToFirebaseStorage(context: Context, fileUri: Uri, fileName: String) {
        val storageRef = Firebase.storage.reference
        val fileReference = storageRef.child("uploads/$fileName")

        val uploadTask = fileReference.putFile(fileUri)

        uploadTask.addOnSuccessListener {
            // File uploaded successfully
            Toast.makeText(context, "File uploaded successfully", Toast.LENGTH_SHORT).show()
        }.addOnFailureListener { exception ->
            // Handle errors
            Toast.makeText(context, "Error uploading file: ${exception.message}", Toast.LENGTH_SHORT).show()
        }
    }

    // Function to download a file from Firebase Cloud Storage
    fun downloadFileFromFirebaseStorage(context: Context, fileName: String, localFile: File) {
        val storageRef = Firebase.storage.reference
        val fileReference = storageRef.child("uploads/$fileName")

        val downloadTask = fileReference.getFile(localFile)

        downloadTask.addOnSuccessListener {
            // File downloaded successfully
            Toast.makeText(context, "File downloaded successfully", Toast.LENGTH_SHORT).show()
        }.addOnFailureListener { exception ->
            // Handle errors
            Toast.makeText(context, "Error downloading file: ${exception.message}", Toast.LENGTH_SHORT).show()
        }
    }

In this example:

  • We use Firebase Cloud Storage as the network storage solution.

  • uploadFileToFirebaseStorage() function uploads a file from the device's local storage to Firebase Cloud Storage.

  • downloadFileFromFirebaseStorage() function downloads a file from Firebase Cloud Storage to the device's local storage.

  • Firebase provides SDKs and libraries that simplify interacting with cloud storage services, handling authentication, security, and network operations.

  1. Example Usage with Retrofit:

    Retrofit is a type-safe HTTP client for Android and Java that simplifies network communication by allowing you to define REST APIs as interfaces. It handles the network operations in a background thread and automatically serializes and deserializes JSON data.

    Let's consider a simple example where we have an API endpoint to fetch user data:

    First, add Retrofit to your project. In your app's build.gradle file, add the Retrofit dependency:

     implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    

    Now, let's define an API interface and set up Retrofit to make network requests:

     import retrofit2.Call
     import retrofit2.http.GET
    
     // Define data model
     data class User(val id: Int, val name: String, val email: String)
    
     // Define Retrofit interface for API endpoints
     interface ApiService {
         @GET("/users")
         fun getUsers(): Call<List<User>>
     }
    
     // Set up Retrofit instance
     val retrofit = Retrofit.Builder()
         .baseUrl("https://api.example.com")
         .addConverterFactory(GsonConverterFactory.create())
         .build()
    
     // Create ApiService instance
     val apiService = retrofit.create(ApiService::class.java)
    
     // Make network request to fetch users
     val call = apiService.getUsers()
     call.enqueue(object : Callback<List<User>> {
         override fun onResponse(call: Call<List<User>>, response: Response<List<User>>) {
             if (response.isSuccessful) {
                 val users = response.body()
                 // Process retrieved users
             } else {
                 // Handle error
             }
         }
    
         override fun onFailure(call: Call<List<User>>, t: Throwable) {
             // Handle failure
         }
     })
    

    In this example:

    • We define a data class User to represent user data received from the API.

    • We define an interface ApiService with a method to fetch users using the @GET annotation.

    • We set up a Retrofit instance with a base URL and added a GSON converter factory to handle JSON serialization and deserialization.

    • We create an instance of the ApiService using the Retrofit instance.

    • We make an asynchronous network request using enqueue() method, and handle the response in callbacks onResponse() and onFailure().

By using Retrofit, we can easily interact with remote APIs to store or retrieve data from network storage. Retrofit simplifies network operations and provides a clean and concise way to define and consume RESTful APIs in Android applications.

Alright devs, It's time to wrap up this blog. I hope this blog helps you to understand Data storage in Android. Ok then we meet on our next blog Types of Functions In Android.


Connect with Me:

Hey there! If you enjoyed reading this blog and found it informative, why not connect with me on LinkedIn? 😊 You can also follow my Instagram page for more mobile development-related content. πŸ“²πŸ‘¨β€πŸ’» Let’s stay connected, share knowledge and have some fun in the exciting world of app development! 🌟

Check out my Instagram page

Check out my LinkedIn

0
Subscribe to my newsletter

Read articles from Mayursinh Parmar directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mayursinh Parmar
Mayursinh Parmar

πŸ“±Mobile App Developer | Android & Flutter πŸŒŸπŸ’‘ Passionate about creating intuitive and engaging apps πŸ’­βœ¨ Let’s shape the future of mobile technology!