Skip to content

05 - Android Data Persistance

Main methods for data persistance

  • Shared preferences
  • Internal storage
  • External storage
  • SQLite Database

Saving data in between same Activity instances

Data forwarding actually!

  • onSaveInstanceState
  • onRestoreInstanceState

Shared preferences

SharedPreferences – save and retrieve key-value pairs

Any primitive data

  • Booleans
  • Floats
  • Ints
  • Longs
  • Strings

To get a SharedPreferences object

  • getSharedPreferences(name, mode) – if you need multiple pref files, identified by name (first parameter)
  • getPreferences(mode) – single pref file for activity, no name specified

Mode

  • MODE_PRIVATE (0) - default
  • MODE_WORLD_READABLE
  • MODE_WORLD_WRITEABLE

To WRITE values

  • Call edit() to get SharedPreferences.Editor
  • Add values with putString(), putBoolean, …
  • Commit values with commit()
1
2
3
4
5
6
7
8
9
override fun onStop() {
    super.onStop()

    val sharedPref = getPreferences(Context.MODE_PRIVATE) ?: return
    with (sharedPref.edit()) {
        putInt(getString(R.string.saved_high_score_key), newHighScore)
        commit()
    }
}

To READ values

Use methods such as getString, getBoolean

1
2
3
4
5
6
7
8
9
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    // ?: - Elvis operator
    val sharedPref = getPreferences(Context.MODE_PRIVATE) ?: return
    val defaultValue = resources.getInteger(R.integer.saved_high_score_default_key)
    val highScore = sharedPref.getInt(getString(R.string.saved_high_score_key), defaultValue)
}

Internal storage

  • Private to application (default)
  • No access for user or other apps
  • When app gets uninstalled – files are deleted with it

Create/Write

Create and Write private file

  • Call openFileOutput(), with filename and operating mode
  • Returns FileOutputStream
  • Write to the file with write()
  • Close the file with close()

Modes

  • MODE_PRIVATE – create (or replace)
  • MODE_APPEND
  • MODE_WORLD_READABLE
  • MODE_WORLD_WRITEABLE

Read

Read from private file

  • Call openFileInput() with filename
  • Returns FileInputStream
  • Get bytes with read()
  • Close stream with close()

Static app files

Save static file during compile time

  • Place in /res/raw
  • Open with OpenRawResource(), passing R.raw.<filename>
  • Returns inputStream
  • File is read-only!!!

File cache

Caching data

  • Do not need data forever
  • getCahceDir()
  • When space is low, Android will delete cache
  • Stay within reasonable space limits (1mb?)
  • Deleted with uninstall
  • Manage cache files yourself

File methods

Other useful methods

  • getFilesDir() - Gets the absolute path to the filesystem directory where your internal files are saved.
  • getDir() - Creates (or opens an existing) directory within your internal storage space.
  • deleteFile() - Deletes a file saved on the internal storage.
  • fileList() - Returns an array of files currently saved by your application.

External storage

All Android devices support shared "external storage"

Can be removable storage media (sd-card)

Or internal, non-removable storage

Files are world-readable

Access

Getting access – manifest

  • READ_EXTERNAL_STORAGE
  • WRITE_EXTERNAL_STORAGE
1
2
3
4
<manifest ...>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    ...
</manifest> 

Availability

Check availability

  • Use getExternalStorageState()

Media might be mounted to a computer, missing, read-only, or in some other state.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Checks if a volume containing external storage is available
// for read and write.
fun isExternalStorageWritable(): Boolean {
    return Environment.getExternalStorageState() == 
        Environment.MEDIA_MOUNTED
}

// Checks if a volume containing external storage is 
// available to at least read.
fun isExternalStorageReadable(): Boolean {
    return Environment.getExternalStorageState() in
            setOf(Environment.MEDIA_MOUNTED, 
                Environment.MEDIA_MOUNTED_READ_ONLY)
}

Public files

  • Files acquired through your app should be saved to a "public" location where other apps can access them and the user can easily copy them from the device
  • Use one of the shared public directories (Music/, Pictures/, and Ringtones/)
  • Use Enviroment.getExternalStoragePublicDirectory()
    DIRECTORY_MUSIC, DIRECTORY_PICTURES, DIRECTORY_RINGTONES

Private

  • Private files (textures, sounds for app, etc) (actually semi-private)
  • Use a private storage directory on the external storage
  • Use getExternalFilesDir()
  • Takes Type, use null when no type
  • From 4.4 onwards does not require permissions
  • Files are hidden from Media Scanner (but not from other apps with permissions)

Database - SQLite

Full support for SQLite

Any database will be accessible by name in any class in app

Private to your app

http://sqlite.org/docs.html

Create db

Create DB

  • Use SQLiteOpenHelper
  • Override onCreate(SQLiteDatabase db)
  • execute a SQLite commands to create tables in the database

The database tables should use the identifier _id for the primary key of the table. Several Android functions rely on this standard.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class DbHelper(context: Context) :
    SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
    companion object {
        const val DATABASE_NAME = "app.db"
        const val DATABASE_VERSION = 2

        const val PERSON_TABLE_NAME = "PERSONS"

        const val PERSON_ID = "_id"
        const val PERSON_FIRSTNAME = "firstName"
        const val PERSON_LASTNAME = "lastName"

        const val SQL_PERSON_CREATE_TABLE =
            "create table $PERSON_TABLE_NAME(" +
                    "$PERSON_ID INTEGER PRIMARY KEY AUTOINCREMENT, " +
                    "$PERSON_FIRSTNAME TEXT NOT NULL, " +
                    "$PERSON_LASTNAME TEXT NOT NULL);"

        const val SQL_DELETE_TABLES = "DROP TABLE IF EXISTS " + 
            "$PERSON_TABLE_NAME";
    }

    override fun onCreate(db: SQLiteDatabase?) {
        db?.execSQL(SQL_PERSON_CREATE_TABLE)
    }

    override fun onUpgrade(db: SQLiteDatabase?, 
        oldVersion: Int, newVersion: Int) {

        db?.execSQL(SQL_DELETE_TABLES)
        onCreate(db)
    }
}

Migration

Override onUpgrade(SQLiteDatabase? db, int oldVersion, int newVersion)

  • Called when the database needs to be upgraded. The implementation should use this method to drop tables, add tables, or do anything else it needs to upgrade to the new schema version.

NB! SQLite does not support dropping columns!

Create new table, copy data, drop old table, rename table, etc...

Open DB

  • Get an instance of your SQLiteOpenHelper implementation using the constructor you've defined
  • To write to db - getWritableDatabase()
  • Read from the db – getReadableDatabase()
  • Both return a SQLiteDatabase object, providing methods for SQLite operations.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class PersonRepository(val context: Context) {

    private lateinit var dbHelper : DbHelper
    private lateinit var db: SQLiteDatabase

    fun open(): PersonRepository {
        dbHelper = DbHelper(context)
        db = dbHelper.writableDatabase
        return this
    }

    fun close(){
        dbHelper.close()
    }

Data types

  • NULL
  • INTEGER
  • REAL
  • TEXT - database encoding (UTF-8, UTF-16BE or UTF-16LE)
  • BLOB
  • Everything else is mapped into one of these types
    • Boolean – int 0/1
    • DateTime – integer, unix timestap or text
    • ...

http://www.sqlite.org/datatype3.html

Date and time function

Date and time functions

  • date(timestring, modifier, modifier, ...)
  • time(timestring, modifier, modifier, ...)
  • datetime(timestring, modifier, modifier, ...)
  • julianday(timestring, modifier, modifier, ...)
  • strftime(format, timestring, modifier, modifier, ...)

http://www.sqlite.org/lang_datefunc.html

Working with data

SQLiteDatabase provides

  • Insert
  • Update
  • Delete
  • execSQL
  • Queries can be created via
    • rawQuery - directly accepts an SQL select statement as input
    • query - provides a structured interface for specifying the SQL query
    • SQLiteQueryBuilder -  convenience class that helps to build SQL queries

rawQuey

1
val cursor = db.rawQuery("select * from person where _id=?", arrayOf("2"))

You may include ?-s in where clause in the query, which will be replaced by the values from selectionArgs. The values will be bound as Strings.

Query

layout

1
2
3
val cursor = db.query(DATABASE_TABLE, 
    arrayOf(KEY_ROWID, KEY_CATEGORY, KEY_SUMMARY, KEY_DESCRIPTION), 
    null, null, null, null, null)
  • If a condition is not required you can pass null, e.g. for the group by clause.
  • The "whereClause" is specified without the word "where", for example a "where" statement might look like: "_id=19 and summary=?"
  • If you specify placeholder values in the where clause via ?, you pass them as the selectionArgs parameter to the query

Cursor

  • A query returns a Cursor object. A Cursor represents the result of a query and basically points to one row of the query result. This way Android can buffer the query results efficiently; as it does not have to load all data into memory.
  • To get the number of elements of the resulting query use the getCount() method.
  • To move between individual data rows, you can use the moveToFirst() and moveToNext() methods. The isAfterLast() method allows to check if the end of the query result has been reached.
  • Cursor provides typed get*() methods, e.g. getLong(columnIndex), getString(columnIndex) to access the column data for the current position of the result. The "columnIndex" is the number of the column you are accessing.
  • Cursor also provides the getColumnIndexOrThrow(String) method which allows to get the column index for a column name of the table.
  • A Cursor needs to be closed with the close() method call.

Insert data

public long insert (String table, String nullColumnHack, ContentValues values)

  • table - the table to insert the row into
  • nullColumnHack - optional; may be null. SQL doesn't allow inserting a completely empty row without naming at least one column name. If your provided values is empty, no column names are known and an empty row can't be inserted. If not set to null, the nullColumnHack parameter provides the name of nullable column name to explicitly insert a NULL into in the case where your values is empty.
  • values - this map contains the initial column values for the row. The keys should be the column names and the values the column values
1
2
3
4
5
6
fun add(person: Person){
    val contentValues = ContentValues()
    contentValues.put(DbHelper.PERSON_FIRSTNAME, person.firstName)
    contentValues.put(DbHelper.PERSON_LASTNAME, person.lastName)
    db.insert(DbHelper.PERSON_TABLE_NAME, null, contentValues)
}

Update

public int update (String table, ContentValues values, String whereClause, String[] whereArgs)

  • table - the table to update in
  • values - a map from column names to new column values. null is a valid value that will be translated to NULL.
  • whereClause - the optional WHERE clause to apply when updating. Passing null will update all rows.
  • whereArgs - You may include ?s in the where clause, which will be replaced by the values from whereArgs. The values will be bound as Strings.
1
2
3
4
5
6
7
fun update(person: Person){
    val contentValues = ContentValues()
    contentValues.put(DbHelper.PERSON_FIRSTNAME, person.firstName)
    contentValues.put(DbHelper.PERSON_LASTNAME, person.lastName)
    db.update(DbHelper.PERSON_TABLE_NAME, contentValues, 
        DbHelper.PERSON_ID + "=?" , arrayOf(person.id.toString()))
}

Delete

public int delete (String table, String whereClause, String[] whereArgs)

  • table - the table to delete from
  • whereClause - the optional WHERE clause to apply when deleting. Passing null will delete all rows.
  • whereArgs - You may include ?s in the where clause, which will be replaced by the values from whereArgs. The values will be bound as Strings.

App Data Architecture

BUT WHAT ABOUT...

  • Room (ORM)
  • LiveData (Observables)
  • Databinding (bind layout to data sources)
    • aka Jetpack libraries

Will be in separate lecture after basics is covered

Room

  • Room – ORM on top of SQLite
  • Room provides an abstraction layer over SQLite to allow fluent database access while harnessing the full power of SQLite.
  • In case of SQLite, there is no compile time verification of raw SQLite queries. In Room there is SQL validation at compile time.
  • As schema changes, you need to update the affected SQL queries manually. Room generates queries, so no manual updates.
  • SQLite has lot’s of boilerplate code to convert between SQL and Objects. Room has this built-in.
  • Room is built to work with LiveData and RxJava for data observation, while SQLite is not.

LiveData 1

  • LiveData is an observable data holder class.
  • Ensures your UI matches your data state
    • LiveData follows the observer pattern. LiveData notifies Observer objects when underlying data changes. You can consolidate your code to update the UI in these Observer objects.
  • No memory leaks
    • Observers are bound to Lifecycle objects and clean up after themselves when their associated lifecycle is destroyed.
  • No crashes due to stopped activities
    • If the observer's lifecycle is inactive, such as in the case of an activity in the back stack, then it doesn’t receive any LiveData events.
  • No more manual lifecycle handling
    • UI components just observe relevant data and don’t stop or resume observation.

LiveData 2

  • Always up to date data
    • If a lifecycle becomes inactive, it receives the latest data upon becoming active again. For example, an activity that was in the background receives the latest data right after it returns to the foreground.
  • Proper configuration changes
    • If an activity or fragment is recreated due to a configuration change, like device rotation, it immediately receives the latest available data.
  • Sharing resources
    • You can extend a LiveData object using the singleton pattern to wrap system services so that they can be shared in your app.

Data binding

The Data Binding Library is a support library that allows you to bind UI components in your layouts to data sources in your app using a declarative format rather than programmatically.

1
<TextView android:text="@{viewmodel.userName}" />

App Architecture

  • Room
  • LiveData
  • Databinding

layout