This library aims to manage datastore in a consistent way.
A datastore is required to extend the BackupRestoreStorage
class and implement either Observable
or KeyedObservable
interface, which enforces:
The Android backup framework provides BackupAgentHelper and BackupHelper to back up a datastore. However, there are several caveats when implement BackupHelper
:
ParcelFileDescriptor
state parameters are normally ignored and data is updated even there is no change.BackupHelper
prevents other BackupHelper
s from restoring data.restoreEntity
(e.g. BatteryBackupHelper), which is not robust sometimes.This library provides more clear API and offers some improvements:
BackupRestoreEntity
interface. The InputStream
of restore will ensure bounded data are read, and close the stream will be no-op.InputStream
and OutputStream
. Actually, the checksum is computed in this way by CheckedInputStream and CheckedOutputStream, see BackupRestoreStorage
implementation for more details.BackupRestoreFileStorage
class, and BackupRestoreFileArchiver
will treat each file as an entity and do the backup / restore.BackupManager.dataChanged
call is unnecessary now, the library will do the invocation (see next section).Manual BackupManager.dataChanged
call is required by current backup framework. In practice, it is found that SharedPreferences
usages foget to invoke the API. Besides, there are common use cases to log metrics when data is changed. Consequently, observer pattern is employed to resolve the issues.
If the datastore is key-value based (e.g. SharedPreferences
), implements the KeyedObservable
interface to offer fine-grained observer. Otherwise, implements Observable
. The library provides thread-safe implementations (KeyedDataObservable
/ DataObservable
), and Kotlin delegation will be helpful.
Keep in mind that the implementation should call KeyedObservable.notifyChange
/ Observable.notifyChange
whenever internal data is changed, so that the registered observer will be notified properly.
For SharedPreferences
use case, leverage the SharedPreferencesStorage
. To back up other file based storage, extend the BackupRestoreFileStorage
class.
Here is an example of customized datastore, which has a string to back up:
class MyDataStore : ObservableBackupRestoreStorage() { // Another option is make it a StringEntity type and maintain a String field inside StringEntity @Volatile // backup/restore happens on Binder thread var data: String? = null private set fun setData(data: String?) { this.data = data notifyChange(ChangeReason.UPDATE) } override val name: String get() = "MyData" override fun createBackupRestoreEntities(): List<BackupRestoreEntity> = listOf(StringEntity("data")) private inner class StringEntity(override val key: String) : BackupRestoreEntity { override fun backup( backupContext: BackupContext, outputStream: OutputStream, ) = if (data != null) { outputStream.write(data!!.toByteArray(UTF_8)) EntityBackupResult.UPDATE } else { EntityBackupResult.DELETE } override fun restore(restoreContext: RestoreContext, inputStream: InputStream) { data = String(inputStream.readAllBytes(), UTF_8) // NOTE: The library will call notifyChange(ChangeReason.RESTORE) for you } } override fun onRestoreFinished() { // TODO: Update state with the restored data. Use this callback instead "restore()" in case // the restore action involves several entities. // NOTE: The library will call notifyChange(ChangeReason.RESTORE) for you } }
In the application class:
class MyApplication : Application() { override fun onCreate() { super.onCreate(); BackupRestoreStorageManager.getInstance(this).add(MyDataStore()); } }
In the custom BackupAgentHelper
class:
class MyBackupAgentHelper : BackupAgentHelper() { override fun onCreate() { BackupRestoreStorageManager.getInstance(this).addBackupAgentHelpers(this); } override fun onRestoreFinished() { BackupRestoreStorageManager.getInstance(this).onRestoreFinished(); } }