Charakterystyka
SharedPreferences
umożliwiają przechowywanie i zarządzanie prostymi danymi prymitywów tj.: wartości logiczne, liczby całkowite i zmiennoprzecinkowe oraz teksty w postaci pary klucz - wartość
. Dane przechowywane są na urządzeniu w plików xml
niezależnie od cyklu życia aplikacji. Istnieją tak długo dopóki nie zostaną usunięte przez kod programu lub wyczyszczone ręcznie przez użytkownika z danych aplikacji. Aplikacja może posiadać wiele instancji SharedPreferences
, które najczęściej są prywatne lecz mogą być także publiczne dla innych aplikacji.
Przeznaczenie
Nazwa SharedPreferences
może sugerować przeznaczenie do przechowywania informacji nt ustawień użytkownika, co jest po części prawdą. Jednakże wszystkie informacje, które można sprowadzić do postaci klucz - wartość nadają się do umieszczenia w SharedPreferences
. Mogą więc to być ustawienia czy preferencje użytkownika, ale równie dobrze także inne dane stanu aplikacji (np. najlepszy wynik) czy dane autoryzacyjne. Pomimo, że wiele informacji może być technicznie zapisanych w SharedPreferences
(np. konwersja do String
) to nie wszystkie powinny się tam znaleźć. Umieszczając wartości należy kierować się zasadą prostych, pojedynczych informacji (nie kolekcji). W pozostałych przypadkach warto rozważyć alternatywy w postaci m.in. plików i bazy danych.
Implementacja
Aby uzyskać referencję do wskazanego pliku SharedPreferences
należy wywołać metodę getSharedPreferences
lub getPreferences
. Czytanie wartości odbywa się za pomocą metod get danego typu, natomiast zapisywanie wartości przebiega jako transakcja na obiekcie SharedPreferences.Editor
.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
fun getSharedPreferences(name : String) : SharedPreferences {
//must be called on Context
return getSharedPreferences(name, Context.MODE_PRIVATE) //pass public to allow access outside app
}
fun getSharedPreferencesForActivity() : SharedPreferences {
//SharedPreferences file only for this activity
return getPreferences(Context.MODE_PRIVATE)
}
fun write(key : String, value : Int) {
val sharedPref = getSharedPreferences("pl.androidcode.app.FILE_KEY")
val editor = sharedPref.edit()
//do some transactions
editor.putInt(key, value) //or use another method to put different types
editor.commit() //to save immediately or apply to save in background
}
fun read(key : String) : Int {
val sharedPref = getSharedPreferences("pl.androidcode.app.FILE_KEY")
val default = 0
return sharedPref.getInt(key, default)
}
}
Preference
W przypadku standardowych ustawień aplikacji edytowanych przez interfejs graficzny dobrym pomysłem mogłoby być wykorzystanie biblioteki Preference
, która ułatwia pracę z SharedPreferences
poprzez dostarczenie kontrolek widoku dla kluczy ustawień. W tym celu należy stworzyć Fragment rozszerzający PreferenceFragmentCompat
i w metodzie onCreatePreferences
przekazać plik xml
z preferencjami. Wprowadzane zmiany dotyczą globalnych domyślnych ustawień możliwych do uzyskania przez wywołanie getDefaultSharedPreferences
.
class SettingsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
//add Preference Fragment
supportFragmentManager
.beginTransaction()
.replace(R.id.container, SettingsFragment())
.commit()
}
//read some pref when needed, use getDefaultSharedPreferences
fun readPref() : Boolean {
val sharedPref = getDefaultSharedPreferences(this)
return sharedPref.getBoolean("pref2", false)
}
}
class SettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preferences, rootKey)
}
override fun onPreferenceTreeClick(preference: Preference?): Boolean {
return when (preference?.key) {
"pref2" -> {
//do something when pref has changed if needed
true
}
else -> {
super.onPreferenceTreeClick(preference)
}
}
}
//implement more methods to react for interactions
}
<!-- PreferenceScreen must be parent -->
<androidx.preference.PreferenceScreen
xmlns:app="http://schemas.android.com/apk/res-auto">
<Preference
app:key="pref1"
app:title="Some title"
app:summary="More details"/>
<SwitchPreferenceCompat
app:key="pref2"
app:title="Some message"
app:summary="More details"/>
<!-- use more preferences from Preference class and subclasses -->
</androidx.preference.PreferenceScreen>
DataStore
Biblioteka Preference
domyślnie zarząda danymi wykorzystując implementację SharedPreferences
. Nierzadko jednak taka realizacja zapisu i odczytu danych może nie być wystarczająca ponieważ może np. zachodzić potrzeba dodatkowego zapisu danych w chmurze ze względu na synchronizację preferencji między urządzeniami. W takiej sytuacji z pomocą przychodzi PreferenceDataStore
dostępny od wersji systemu Android O
.
//use custom PreferenceDataStore inside onCretePreferences
class DataStoreFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preferences, rootKey)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//enable data store only for specific preference
val preference = findPreference("pref2")
preference.preferenceDataStore = DataStore()
//ore enable for entire hierarchy
preferenceManager.preferenceDataStore = DataStore()
}
}
}
//override only used operations
@RequiresApi(Build.VERSION_CODES.O)
class DataStore : PreferenceDataStore {
//make sure to provide proper non blocking threading during extra work
override fun getBoolean(key: String?, defValue: Boolean): Boolean {
//try to get pref from remote
//if failed or doesn't exists then try to get from local
return super.getBoolean(key, defValue)
//log operation
}
override fun putBoolean(key: String?, value: Boolean) {
//try to put pref to remote
//save also in local
super.putBoolean(key, value)
//log operation
}
}
Bezpieczeństwo
Biblioteka Security
dostarcza dodatkowej ochrony przed niepowołanym dostępem do danych stosując się do kryptograficznych reguł bezpieczeństwa z jednoczesnym zachowaniem wydajności. Wykorzystuje dwuczęściowy system zarządzania kluczami składający się ze zbioru kluczy keyset
oraz z klucza głównego master key
. Keyset
zawiera klucze szyfrujące dane, które są przechowywane w SharedPreferences
, natomiast master key
jest kluczem szyfrującym klucze z keyset
i przechowywany jest w KeyStore
. Dostęp do zaszyfrowanych SharedPreferences
odbywa się przez instancję EncryptedSharedPreferences
.
class EncryptedActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val encryptedSharedPref = createEncryptedSharedPref()
encryptedSharedPref.edit().putInt("key", 100).commit()
val encrypted = encryptedSharedPref.getInt("key", 0) //100
//encrypted and standard SharedPreferences are different despite the same file key declaration
val sharedPref = getSharedPreferences("pl.androidcode.app.FILE_KEY", Context.MODE_PRIVATE)
val normal = sharedPref.getInt("key", 0) //it will be 0
}
fun createEncryptedSharedPref(): SharedPreferences {
return EncryptedSharedPreferences.create(
"pl.androidcode.app.FILE_KEY",
getMasterKeyAlias(),
this,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
fun getMasterKeyAlias(): String {
val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC
return MasterKeys.getOrCreate(keyGenParameterSpec)
}
}