Options:
- Internal storage:
- Small, private to the app
- Files deleted when app removed
- Used by
SharedPreferences,DataStore(newer) which can be used as a lightweight key-value data store
- External/shared storage
- Large data files
- Possibly shared with other apps
- Files persist after app uninstalled
- SQLite database
- The cloud
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:
- Put files in the project
/res/raw/directory - Use
context.openRawResource(R.raw.raw_file_id)to get anInputStream
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:
- Model types are annotated (e.g. with the
@Entitytype attribute) - Create a DAO object that bridges between the database and host language types
- Create an interface for the database (getters for each model’s DAO)
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: