Open menu with table of contents Storage Options
Logo of Stuttgart Media University for light theme Logo of Stuttgart Media University for dark theme
Android Development

Storage Options

Stuttgart Media University

1 Agenda

  • Shared Preferences
  • Internal and External Storage
  • SQLite Database, Room & others
  • Network Connections & cloud storage
  • Assignment 4

2 Shared Preferences

  • Simple way to persistently store small data entities in a key-value storage
  • Useful for data such as user preferences, app settings etc.
  • Access to storage via the class SharedPreferences
    • Obtain an instance by calling getSharedPreferences() if multiple preference files are required. First parameter is the name of the preference file
    • Or by calling getPreferences() if only one preference file is needed
  • To write values to the preferences file the class SharedPreferences.Editor can be used
    • Obtain an instance by calling edit() on the SharedPreferences instance
    • Add values using putBoolean() and putString() with the key as the first parameter and the value as the second parameter
    • Commit the new values with commit()
  • Read values using getBoolean() and getString()

3 Shared Preferences

Example code, reading values in onCreate()

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()

        val settings = getSharedPreferences(PREFS_NAME, 0)
        val silent = settings.getBoolean("silentMode", false)
        setSilent(silent)

        ...
    }
}

4 Shared Preferences

  • Example code, writing values in onStop()
// ...
override fun onStop() {
    super.onStop()

    // We need an Editor object to make preference changes.
    // All objects are from android.context.Context
    val settings = getSharedPreferences(PREFS_NAME, 0)
    val editor = settings.edit()
    editor.putBoolean("silentMode", mSilentMode)

    // Commit the edits!
    editor.commit()
}

// ...

5 Internal Storage

  • Files saved to the internal storage are private to the application
  • The files are removed when the application is uninstalled
  • Use the FileOutputStream to write data to a file
    • To obtain an instance call openFileOutput() with the filename and an operating mode:
      • MODE_PRIVATE will create or replace a file and make it private to your application
      • MODE_APPEND appends to an existing file
    • Write to the file using write()
    • Close the stream with close()

6 Internal Storage

  • Example for writing a file
val FILENAME = "hello_file"
val string = "hello world!"

val fos = openFileOutput(FILENAME, MODE_PRIVATE)
fos.write(string.toByteArray())
fos.close()

7 Internal Storage

  • To read a file use openInputFile() with the name of the file as parameter to retrieve a FileInputStream
  • Read bytes from the stream using read()
  • Then close the stream using close()
  • To open a static resources file, that is stored in the res/raw/ directory, use openRawResource() and pass the filename using R.raw.\<filename>

8 External Storage

  • Android devices support a shared "external storage" to save data. This is either removable SD-cards or internal (non-removable) storage.
  • External storage can be modified by the user or other apps
  • Specific directories for certain data exist, scoped directories, e.g. for Pictures, Music, etc.

9 External Storage

  • In order to read or write to the external storage, the permissions READ_EXTERNAL_STORAGE or WRITE_EXTERNAL_STORAGE must be requested and granted by the user. Do this in the Androidmanifest.xml file:
<manifest ...>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    ...
</manifest>

If both, reading and writing, is required, only the WRITE_EXTERNAL_STORAGE permission is required.

10 External Storage

Before writing to the external storage, make sure that it is available by calling getExternalStorageState():

/**
 * Checks if external storage is available for read and write 
 */
fun isExternalStorageWritable(): Boolean {
    val state = Environment.getExternalStorageState()
    if (Environment.MEDIA_MOUNTED == state) {
        return true
    }
    return false
}

11 External Storage

  • Retrieve the path to a public directory by calling getExternalStoragePublicDirectory()
    • Pass the type of directory as parameter, e.g. DIRECTORY_MUSIC or DIRECTORY_PICTURES
fun getAlbumStorageDir(albumName: String?): File {
    // Get the directory for the user's public pictures directory.
    val file = File(
        Environment.getExternalStoragePublicDirectory(
            Environment.DIRECTORY_PICTURES
        ), albumName
    )
    if (!file.mkdirs()) {
        Log.e(LOG_TAG, "Directory not created")
    }
    return file
}

12 External Storage

  • To save app private files retrieve the path by calling getExternalFilesDir()
    • The parameter is again the type (e.g. DIRECTORY_MOVIES) or null if a specific media directory is not necessary
  • Since API Level 18 (Android 4.4) writing to app private directories does not require the WRITE_EXTERNAL_STORAGE permission
  • When the user uninstalls the app, the directory and all its content is deleted
  • The MediaStore ContentProvider does not scan private directories for media
  • Beware: files are readable by other apps through the same API

13 SQLite Databases

  • Android provides full support for SQLite databases
  • To create a new SQLite database, create a subclass of SQLiteOpenHelper and override the onCreate() method
    • In there execute a SQLite command to create tables in the database

14 SQLite Databases

class DictionaryOpenHelper internal constructor(context: Context?) :
    SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
    override fun onCreate(db: SQLiteDatabase) {
        db.execSQL(CREATE)
    }

    override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
        db?.execSQL(UPGRADE)
    }
}

15 SQLite Databases

  • Get an instance of the SQLiteOpenHelper using the defined constructor
  • Write to the database by calling getWritableDatabase()
  • Read from the database by calling getReadableDatabase()
    • Both return a SQLiteDatabase object that provides methods for SQLite operations
  • Execute a SQLite query using the SQLiteDatabase query() methods
  • For complex queries use the SQLiteQueryBuilder
  • The query returns a Cursor that points to the rows found by the query
  • The Android SDK includes a sqlite3 database tool for DB debugging. You can find it on macOs under /Users/\<username>/Library/Android/sdk/platform-tools

16 Room

  • Room Library
  • Object Relational Mapping (ORM) for SQLite optimised for Android.
  • Layer on top of SQLite.
  • Local databases are usually used to cache remote data for offline usage. Therefore syncing offline and online data should be your concern, not database structure.
  • Available via Android Jetpack

17 Room gradle dependencies

Ad this dependency to your app build.gradle.kts:

implementation(libs.androidx.room.gradle.plugin)
implementation(libs.androidx.room.runtime)
annotationProcessor(libs.androidx.room.compiler)
ksp(libs.androidx.room.compiler)
implementation(libs.androidx.room.ktx)

18 Entity

  • Defines database table structure via plain old java objects (POJO)
  • Entity class can be reused with other libraries like Retrofit
  • Must have a primary key
  • Is defined via @Entity annotation

19 Entity Example

Use @PrimaryKey(autoGenerate = true) to let Room create and auto increment unique ids for your entries.

@Entity
data class Movie(
    @PrimaryKey(autoGenerate = true) val uid: Int = 0,
    @ColumnInfo(name = "title") val title: String,
    @ColumnInfo(name = "year") val year: String,
    @ColumnInfo(name = "poster") val poster: String
)

20 Data access object

  • Provides an interface to the database.
  • Must be implemented as interface or abstract class.
  • Room generates the DAO implementation during compiletime.
  • Good point to abstract local and remote database handling.
  • @Insert, @Delete etc. use suspend to indicate that this opration runs in the background because those can take longer
  • Offers methods to run native SQL queries.
  • Offers convenience methods for inserting and deleting data.
  • Can be mocked for testing purpose.
  • Integrates well with Android LifeData or RX.

21 DAO Example

import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query

@Dao
interface MovieDao {

    @Query("SELECT * FROM movie")
    fun getAllMovies(): List<Movie>

    @Insert
    suspend fun insert(movie: Movie)

    @Delete
    suspend fun delete(movie: Movie)
}

22 Alternative databases

23 Database

  • Holds the link to the native Android SQLiteOpenHelper.
  • Covers the complete persistence level underneath generated code.
  • Room generates the database implementation during compile time.
  • Must be an abstract class.
  • Must include a list of used entities.
  • Database access from main thread is disabled by default. Must be specifically enabled via allowMainThreadQueries()

24 Database Example

@Database(entities = [Movie::class], version = 1, exportSchema = false)
abstract class MovieDatabase : RoomDatabase() {

    abstract fun movieDao(): MovieDao

    companion object {
        @Volatile
        private var Instance: MovieDatabase? = null

        fun getDatabase(context: Context): MovieDatabase {
            return Instance ?: synchronized(this) {
                Room.databaseBuilder(context, MovieDatabase::class.java, "movie-database")
                    .build()
                    .also { Instance = it }
            }
        }
    }
}

25 Room Example

Insert an entity into database. This is a suspend operation which needs to run in a background thread so use coroutines:

lifecycleScope.launch(Dispatchers.IO) {
    MovieDatabase.getDatabase(applicationContext).movieDao().insert(movie)

    withContext(Dispatchers.Main) {
        refresh()
    }
}

26 Room Example

Get a list of entities from database:

lifecycleScope.launch(Dispatchers.IO) {
    val movies = MovieDatabase.getDatabase(applicationContext).movieDao().getAllMovies()
    Log.d("TAG", movies[0].title)

    withContext(Dispatchers.Main) {
        refresh()
    }
}

27 Assignment 4

28 Cloud Strage Providers

  • There are many cloud storage providers that you could use to store data on a server. Some of them also provide libraries and APIs that make life easier. Examples are:
  • Do not use them in the project!

29 Summary

  • In order to persist data on an Android device you have many options:
  • Little information such as settings can be stored using the SharedPreferences
  • Raw data such as images or other generated filed can be stored directly on the file system
  • App specific data, such as content, or user generated data can be stored in databases, such as the SQLite DB or other storages
    • ORM frameworks make life easier for the developer
  • Cloud storage is accessible via networking classes or cloud storage SDKs that do the magic for you

30 Recap Questions

  • What kind of data would you store how?
  • When is the best time to persist data?
  • What is the best way to load data from a storage?
  • Where would you place the code in an Android app?
  • In which cases is cloud storage necessary?

Questions?