Jetpack series of articles
- Chapter 1 “Jetpack Lifecycle for Android front-end and back-end switching detection”
- Chapter 2 “Jetpack DataStore of Android PreferenceFragmentCompat”
This is a series of Jetpack articles, and there are still many that I don’t have time to write. Anyway, I will record a few if I have time. If you have any questions, leave a message and I will reply!
Article directory
- Jetpack series of articles
- Preface
-
- Understand the key difference between DataStore and SharedPreferences
- 1. DataStore novice configuration
-
- 1. Import the library
- 2.Initialization
- 2. DataStore reads and writes Value through Key
- 3. DataStore practical example
-
- 1. Convert DataStore into data class Settings
- 2. Create ViewModel and use PreferencesRepository
- 3. Use PreferencesViewModel in Activity
- 4. Display effect
- 4. PreferenceFragmentCompat displays UI based on Datastore data
-
- 1. Import the library
- 2. Create PreferenceFragmentCompat layout
- 3. Create PreferenceDataStoreAdapter (key)
- 4. Create SettingsPreferencesFragment and use PreferenceDataStoreAdapter
- 5. Modify TestActivity to add SettingsButton to jump to SettingsPreferencesFragment
- 6.Display effect
- Summarize
Foreword
This article mainly talks about using DataStore
to replace SharedPreferences
, and you can use PreferenceFragmentCompat
to quickly configure UI
to modify preferences. Let me say a few nonsense words here. I noticed DataStore
a long time ago. It seems to be a little different from SharedPreferences
, but after using it, I feel that it is still good. Well, Flow
using kotlin
needs to be used with coroutines. The previous projects at hand were all written in java
and all used SharedPreferences
, I recently upgraded the project and thought about changing SharedPreferences to DataStore
. I don’t know how it will be messed up as soon as I use it (liao). It obviously feels better than SharedPreferences
It’s a bit more complicated, let me talk about some of the problems I encountered.
- How does DataStore edit and obtain Value through Key like SharedPreferences?
- DataStore does not support creating multiple instances?
- How does DataStore monitor Value changes?
- How does DataStore use PreferenceFragmentCompat to read and modify preference values?
Understand the key differences between DataStore and SharedPreferences
The Key
of DataStore
is Preferences.Key<*>
, and the key
of SharedPreferences
is String
, the supported storage data types are the same, but the official has upgraded the Key
of DataStore
, and changed the keyname
is tied to the data type, which is equivalent to declaring what data type this key
will store when declaring keyname
. This approach avoids accidental Save it as other data types. For example, when you obviously want to save an int
, you accidentally pass a string when passing the value, so you save it as String
(when I just developed Android I have encountered it)
DataStore defaultPreferencesKeys
intPreferencesKey("keyname")
representsInt
typedoublePreferencesKey("keyname")
represents theDouble
typestringPreferencesKey("keyname")
represents theString
typebooleanPreferencesKey("keyname")
represents theBoolean
typefloatPreferencesKey("keyname")
represents theFloat
typelongPreferencesKey("keyname")
representsLong
typestringSetPreferencesKey("keyname")
represents theSet
type
It looks very simple, don’t be afraid, just convert the key
of SharedPreferences
into Preferences.Key
through the function corresponding to the data type above. I don’t understand. If so, let’s go down and see how to actually use it, GOGOGO ~
1. DataStore novice configuration
1. Import library
implementation("androidx.datastore:datastore-preferences:1.0.0")
2.Initialization
Create a new PreferencesRepository.kt file
val Context.settings: DataStore<Preferences> by preferencesDataStore("settings") class PreferencesRepository(context: Context) {<!-- --> val settings = context.settings }
Let me explain, I created a PreferencesRepository
class, and created a Context.settings
extension variable outside the class with the file name settings
Instance of DataStore
, this has the advantage that you can get the same instance of DataStore
in any place with Context
to avoid triggering the same file Error being initialized in multiple places
2. DataStore reads and writes Value through Key
val Context.settings: DataStore<Preferences> by preferencesDataStore("settings") class PreferencesRepository(context: Context) {<!-- --> //Declare all KEYs to be used private object PreferencesKeys {<!-- --> val USER_NAME = stringPreferencesKey("username") } //Get settings from Context private val settings = context.settings /** * write */ suspend fun setUserName(text: String) {<!-- --> settings.edit {<!-- --> it[PreferencesKeys.USER_NAME] = text } } /** * read */ fun getUserNameFlow(): Flow<String> {<!-- --> return settings.data.map {<!-- --> it[PreferencesKeys.USER_NAME] ?: "Nothing" } } }
DataStore
requires the use of coroutines for reading and writing. You can also use RxJava
on the Android
official website to use DataStore
, I am not used to RxJava
, so I just use kotlin
to write it. This is just a simple example. I will demonstrate the actual application in more detail below.
3. DataStore practical example
1. Convert DataStore into data class Settings
val Context.settings: DataStore<Preferences> by preferencesDataStore("settings") data class Settings(val username: String, val themes: String) class PreferencesRepository(context: Context) {<!-- --> //Declare all KEYs to be used private object PreferencesKeys {<!-- --> val USER_NAME = stringPreferencesKey("username") val THEMES = stringPreferencesKey("themes") } //Get settings from Context private val settings = context.settings //Integrate settings data into data classes val settingsFlow = settings.data .catch {<!-- --> exception -> if (exception is IOException) {<!-- --> //If an IO exception occurs while reading the DataStore file, an empty file will be returned. emptyPreferences() } else {<!-- --> //If it is other exceptions, throw an exception throw exception } }.map {<!-- --> //Convert the contents of the DataStore file into the flow Flow<Settings> to facilitate conversion to LiveData val username = it[PreferencesKeys.USER_NAME] ?: "" val themes = it[PreferencesKeys.THEMES] ?: "0" Settings(username, themes) } /** * write */ suspend fun setUserName(text: String) {<!-- --> settings.edit {<!-- --> it[PreferencesKeys.USER_NAME] = text } } /** * read */ fun getUserNameFlow(): Flow<String> {<!-- --> return settings.data.map {<!-- --> it[PreferencesKeys.USER_NAME] ?: "Nothing" } } }
2. Create ViewModel and use PreferencesRepository
class PreferencesViewModel(context: Context) : ViewModel() {<!-- --> private val repository = PreferencesRepository(context) private val settingsFlow = repository.settingsFlow //Convert settingsFlow to LiveData val settings = settingsFlow.asLiveData() fun setUserName(text: String) {<!-- --> viewModelScope.launch {<!-- --> repository.setUserName(text) } } //get directly fun getUserName(): String {<!-- --> return runBlocking {<!-- --> repository.getUserNameFlow().first() } } //Observe UserName changes through LiveData fun getUserNameLiveData(): LiveData<String> {<!-- --> return repository.getUserNameFlow().asLiveData() } classFactory( private val context: Context ) : ViewModelProvider.Factory {<!-- --> override fun <T : ViewModel> create(modelClass: Class<T>): T {<!-- --> if (modelClass.isAssignableFrom(PreferencesViewModel::class.java)) {<!-- --> @Suppress("UNCHECKED_CAST") return PreferencesViewModel(context) as T } throw IllegalArgumentException("Unknown ViewModel class") } } }
3. Use PreferencesViewModel in Activity
class TestActivity : AppCompatActivity() {<!-- --> private lateinit var binding: ActivityTestBinding private lateinit var viewModel: PreferencesViewModel override fun onCreate(savedInstanceState: Bundle?) {<!-- --> super.onCreate(savedInstanceState) binding = ActivityTestBinding.inflate(layoutInflater) setContentView(binding.root) viewModel = ViewModelProvider( this, PreferencesViewModel.Factory(this) )[PreferencesViewModel::class.java] //Get username directly val userName = viewModel.getUserName() Log.d("TestActivity", "getUserName: $userName") //Observe userName changes through LiveData viewModel.getUserNameLiveData().observe(this) {<!-- --> value -> Log.d("TestActivity", "observe userName: $value") } //Observe changes in settings data class through LiveData viewModel.settings.observe(this) {<!-- --> Log.d("TestActivity", "observe settings: $it") } //Hurry up and try setting the username viewModel.setUserName("Felix") binding.save.setOnClickListener {<!-- --> //Save the entered user name viewModel.setUserName(binding.username.text.toString()) } } }
Layout section
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/white"> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:layout_marginTop="200dp" android:orientation="horizontal"> <EditText android:id="@ + id/username" android:layout_width="200dp" android:layout_height="wrap_content" android:hint="User Name" /> <com.google.android.material.button.MaterialButton android:id="@ + id/save" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Save" /> </LinearLayout> </FrameLayout>
4. Display effect
4. PreferenceFragmentCompat displays UI based on Datastore data
1. Import library
implementation("androidx.preference:preference:1.2.1")
2. Create PreferenceFragmentCompat layout
Create a fragment_settings_prefernces.xml in the project app/main/res/layout folder
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto" android:background="@color/white"> <androidx.appcompat.widget.Toolbar android:id="@ + id/toolbar" android:layout_width="match_parent" app:navigationIcon="@drawable/ic_left_white_back" android:layout_height="wrap_content" android:background="#ff5722" /> <FrameLayout android:id="@ + id/content" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginTop="?actionBarSize" /> </FrameLayout>
Create a preferences_arrays.xml in the project app/main/res/values folder
<?xml version="1.0" encoding="utf-8"?> <resources> <string-array name="themes_labels"> <item>Default</item> <item>Saint Laurent Purple</item> <item>Pikachu yellow</item> <item>Super Vibrant Orange</item> </string-array> <string-array name="themes_values"> <item>0</item> <item>1</item> <item>2</item> <item>3</item> </string-array> <string name="default_themes_value">0</string> </resources>
Create settings_preferences.xml in the project app/main/res/xml folder
<?xml version="1.0" encoding="utf-8"?> <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <ListPreference android:defaultValue="@integer/default_themes_value" android:entryValues="@array/themes_values" android:key="themes" app:entries="@array/themes_labels" app:title="Theme settings" /> <EditTextPreference android:key="username" app:summary="It seems that generally this will not appear in the settings, hahaha, this is an example for reference only" app:title="username" /> </PreferenceScreen>
3. Create PreferenceDataStoreAdapter (key)
class PreferenceDataStoreAdapter( private val dataStore: DataStore<Preferences>, private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) ) : PreferenceDataStore() {<!-- --> override fun getBoolean(key: String, defValue: Boolean): Boolean {<!-- --> return runBlocking {<!-- --> dataStore.data.map {<!-- --> it[booleanPreferencesKey(key)] ?: defValue }.first() } } override fun putBoolean(key: String, value: Boolean) {<!-- --> scope.launch {<!-- --> dataStore.edit {<!-- --> it[booleanPreferencesKey(key)] = value } } } override fun getString(key: String, defValue: String?): String? {<!-- --> return runBlocking {<!-- --> dataStore.data.map {<!-- --> it[stringPreferencesKey(key)] ?: defValue }.first() } } override fun putString(key: String, value: String?) {<!-- --> scope.launch {<!-- --> dataStore.edit {<!-- --> it[stringPreferencesKey(key)] = value ?: "" } } } override fun getInt(key: String, defValue: Int): Int {<!-- --> return runBlocking {<!-- --> dataStore.data.map {<!-- --> it[intPreferencesKey(key)] ?: defValue }.first() } } override fun putInt(key: String, value: Int) {<!-- --> scope.launch {<!-- --> dataStore.edit {<!-- --> it[intPreferencesKey(key)] = value } } } override fun getFloat(key: String, defValue: Float): Float {<!-- --> return runBlocking {<!-- --> dataStore.data.map {<!-- --> it[floatPreferencesKey(key)] ?: defValue }.first() } } override fun putFloat(key: String, value: Float) {<!-- --> scope.launch {<!-- --> dataStore.edit {<!-- --> it[floatPreferencesKey(key)] = value } } } override fun getLong(key: String, defValue: Long): Long {<!-- --> return runBlocking {<!-- --> dataStore.data.map {<!-- --> it[longPreferencesKey(key)] ?: defValue }.first() } } override fun putLong(key: String, value: Long) {<!-- --> scope.launch {<!-- --> dataStore.edit {<!-- --> it[longPreferencesKey(key)] = value } } } override fun getStringSet(key: String, defValues: MutableSet<String>?): Set<String>? {<!-- --> return runBlocking {<!-- --> dataStore.data.map {<!-- --> it[stringSetPreferencesKey(key)] ?: defValues }.first() } } override fun putStringSet(key: String, values: MutableSet<String>?) {<!-- --> scope.launch {<!-- --> dataStore.edit {<!-- --> it[stringSetPreferencesKey(key)] = values ?: emptySet() } } } }
4. Create SettingsPreferencesFragment and use PreferenceDataStoreAdapter
class SettingsPreferencesFragment : PreferenceFragmentCompat() {<!-- --> private lateinit var binding: FragmentSettingsPreferncesBinding override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {<!-- --> preferenceManager.preferenceDataStore = PreferenceDataStoreAdapter(requireContext().settings, lifecycleScope) setPreferencesFromResource(R.xml.settings_preferences, rootKey) } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View {<!-- --> //Customize PreferenceFragmentCompat layout binding = FragmentSettingsPreferncesBinding.inflate(inflater, container, false) binding.content.addView(super.onCreateView(inflater, container, savedInstanceState), -1, -1) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) {<!-- --> super.onViewCreated(view, savedInstanceState) binding.toolbar.setNavigationOnClickListener {<!-- --> requireActivity().supportFragmentManager.beginTransaction() .remove(this) .commitAllowingStateLoss() } } }
5. Modify TestActivity to add SettingsButton to jump to SettingsPreferencesFragment
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/white"> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:layout_marginTop="200dp" android:orientation="horizontal"> <EditText android:id="@ + id/username" android:layout_width="200dp" android:layout_height="wrap_content" android:hint="User Name" /> <com.google.android.material.button.MaterialButton android:id="@ + id/save" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Save" /> <com.google.android.material.button.MaterialButton android:id="@ + id/settings" android:layout_width="wrap_content" android:layout_marginStart="50dp" android:layout_height="wrap_content" android:text="Settings" /> </LinearLayout> <androidx.fragment.app.FragmentContainerView android:id="@ + id/fragment_content" android:layout_width="match_parent" android:layout_height="match_parent"/> </FrameLayout>
class TestActivity : AppCompatActivity() {<!-- --> private lateinit var binding: ActivityTestBinding private lateinit var viewModel: PreferencesViewModel override fun onCreate(savedInstanceState: Bundle?) {<!-- --> super.onCreate(savedInstanceState) binding = ActivityTestBinding.inflate(layoutInflater) setContentView(binding.root) viewModel = ViewModelProvider( this, PreferencesViewModel.Factory(this) )[PreferencesViewModel::class.java] //Get username directly val userName = viewModel.getUserName() Log.d("TestActivity", "getUserName: $userName") //Observe userName changes through LiveData viewModel.getUserNameLiveData().observe(this) {<!-- --> value -> Log.d("TestActivity", "observe userName: $value") binding.username.setText(value) } //Observe changes in settings data class through LiveData viewModel.settings.observe(this) {<!-- --> Log.d("TestActivity", "observe settings: $it") } //Hurry up and try setting the username viewModel.setUserName("Felix") binding.save.setOnClickListener {<!-- --> //Save the entered username viewModel.setUserName(binding.username.text.toString()) } binding.settings.setOnClickListener {<!-- --> supportFragmentManager.beginTransaction() .add( R.id.fragment_content, SettingsPreferencesFragment(), SettingsPreferencesFragment::class.simpleName ) .commitAllowingStateLoss() } } }
6. Display effect
Summary
It seems troublesome, but it’s actually okay. It’s just that I posted a lot of code in order to write more details. DataStore
is more convenient to cooperate with ViewModel
than SharedPreferences
. DataStore uses asynchronous reading and writing to avoid ANR
(although I Never encountered it, official said so). Before writing this article, I had been stuck in the PreferenceFragmentCompat
display, because the method provided by PreferenceDataStore
did not directly support coroutines. After reading the Android information, I learned that you can use runBlocking
to execute the suspend function in a non-coroutine context function, but please note that we must use first()
function and cannot use last()
, to avoid causing ANR
. I won’t go into details here. I don’t want to write all of this in one article. I’m afraid it will be too confusing. I want to understand You can check the information for details.
Finally, I encountered a problem during use:
The ListPreference
control in the PreferenceFragmentCompat
layout does not support storing int. This is a defect of the official control. I have submitted a report to the official for follow-up. If I want to solve this problem now, there are two ways There are two ways. The first one is to change the original storage of int to storage string. The second one is to customize a ListPreference
control. The problem is that ListPreference
The >entryValues property is read as CharSequence[] mEntryValues
, so no matter what array we pass it will become a string array, and when the value is stored, it will be stored as a string. This is the problem. .
I proposed this solution in an article “Android solves the problem that PreferenceFragmentCompat cannot save Int”
Don’t worry about `ui`, focus on using `DataStore`
If you have any questions or better ideas, please leave a message below and learn from each other.