06. Persisting Data

Options:

Bundles

On rotation, an activity is recreated and hence, we must ensure that state is restored. We use Bundles to transfer information between activities, or between instances of the same activity.

/// Called after `onStart` if a bundle exists
/// Same parameter passed in `onCreate`, but null checking is required, and it
/// may be more convenient to do the state restoration after all initialization
/// has been done or to allow subclasses to override it
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
  savedInstanceState?.run {
    someStateValue = getString(SOME_KEY)
  }
}

override fun onSaveInstanceState(outState: Bundle?) {
  savedInstanceState?.run {
    putString(SOME_KEY, someStateValue)
  }
}

Internal Storage

We can write to a private (not accessible to other apps) internal storage directory using Context.MODE_PRIVATE.

In Android 10 and higher, the directory location is encrypted. The security API can be used to encrypt files in older versions.

context.openFileOutput(filename, Context.MODE_PRIVATE).use {
  it.write(fileContents.toByteArray())
}

To get the absolute path to the directory, use context.getFilesDir().

context.getDir can be used to get (and if necessary, create) a directory.

context.fileList() returns an array of strings with files associated with the context.

Static Files

Read-only static data which is generated at or before compile time:

Cache files

Use context.getCacheDir() to get a File object which is a reference to a directory where you can create and save temporary files.

The Android system may delete them later if space is needed. However, you should still clean up cache files on yourself (context.deleteFile(name: String)).

SharedPreferences

A key-value store backed by an XML file in the internal storage directory.

Jetpack DataStore

Co-routines used to store data asynchronously. While the Preference DataStore is similar to SharedPreferences, the Proto DataStore can be used to store data as custom data types (and hence, it can perform type-checking).

ExternalStorage

getExternalFilesDir(type: String?) returns the primary shared/external data storage device. There is no security - any app with the WRITE_EXTERNAL_STORAGE permission can write and overwrite.

The type can be null to get the root directory, or Environment.DIRECTORY_$type where type is something such as DCIM, DOWNLOADS, or SCREENSHOTS.

Shareable Storage Directories

For media content not owned by your app which you wish to be accessible through the MediaStore API.

ORM

Object-Relational Mapping (ORM):

Automatic mapping of models in code to relational database tables. This is found in various platforms:

Platform Main ORM implementation
Android Room
Ruby on Rails ActiveRecord
Java Hibernate
iOS CoreData

Room

Process:

def room_version = "2.2.6"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"

// For Kotlin extension and coroutine support
implementation "androidx.room:room-ktx:$room_version"

Example:

@Entity
data class User(
  @PrimaryKey val id: Int,
  @ColumnInfo(name = "first_name") val firstName: String?,
  @ColumnInfo(name = "last_name") val lastName: String?,
  @Ignore val profileImage: Bitmap?,
)


@Dao
interface Dao {
  @Query("SELECT * FROM user")
  fun getAll(): List<User>

  @Query("SELECT * FROM user WHERE id IN (:userIds)")
  fun getAllWithId(userIds: IntArray): List<User>

  @Query("SELECT * FROM user WHERE first_name LIKE :firstName AND " +
         "last_name LIKE :lastName LIMIT 1")
  fun findByName(first: String, last: String): User

  @Insert
  fun insertAll(vararg users: User)

  @Delete
  fun delete(user: User)
}


@Database(entities = arrayOf(User::class), version = 1)
abstract class AppDatabase: RoomDatabase() {
  abstract fun userDao(): UserDao
}


val db = Room.databaseBuilder(applicationContext,
                              AppDatabase::class.java, "database-name").build()

val userDao = db.userDao()
val users: List<User> = userDao.getAll()

View Models

A helper class which prepares data for the UI. It is lifecycle aware - it is retained during configuration changes (e.g. orientation change), and hence can replace the use of bundles in activities.

implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'

An activity can have a ViewModel which it uses to share data between fragments:

private val viewModel: MyAppViewModel by viewModels()

The ViewModel can contain LiveData - observable data which can trigger UI updates. LiveData observables can be mapped to Compose state using androidx.compose.runtime:runtime-livedata and the LiveData.observeAsState() method: