Jetpack 系列文章
- 第一章《Android 前台后台切换检测之Jetpack Lifecycle》
- 第二章《Android 首选项PreferenceFragmentCompat之Jetpack DataStore》
这是Jetpack 系列文章,还有好多没时间写,反正有时间我就记录一点,有什么问题留言后看到会回复!🙂
文章目录
前言
这篇主要讲的是用DataStore
取代SharedPreferences
,并且可使用PreferenceFragmentCompat
快速配置UI
修改首选项。在这里容我先说两句废话,DataStore
我在很久之前就注意到了,看上去与SharedPreferences
使用还是有点区别的,不过用上了就觉得还好,就是使用kotlin
的Flow
需要配合协程使用,手上之前的项目都是java
写的也都是使用SharedPreferences
,最近来升级项目想着刚好把SharedPreferences改成DataStore
,不用不知道一用就乱了(liao),明显感觉比SharedPreferences
复杂一点,我来说说我遇到的几个问题吧
- DataStore如何像SharedPreferences通过Key编辑和获取Value?
- DataStore不支持创建多个实例?
- DataStore怎么监听Value变化?
- DataStore如何使用PreferenceFragmentCompat读取和修改首选项值?
了解DataStore和SharedPreferences的Key区别
DataStore
的Key
是Preferences.Key<*>
,SharedPreferences
的key
是String
,支持的存储数据类型都一样,只不过过呢官方把DataStore
的Key
升级了一下,把keyname
和数据类型绑在一起,相当于在声明keyname
的时候声明了这个key
会存储什么样的数据类型,这样的做法避免了不小心存成其他数据类型,比如明明要存一个int
结果传值的时候不小心传了一个字符串,就存成String
了(在刚开发Android的我就碰到过😭)
DataStore默认PreferencesKeys
intPreferencesKey(”keyname“)
代表Int
型doublePreferencesKey(”keyname“)
代表Double
型stringPreferencesKey(”keyname“)
代表String
型booleanPreferencesKey(”keyname“)
代表Boolean
型floatPreferencesKey(”keyname“)
代表Float
型longPreferencesKey(”keyname“)
代表Long
型stringSetPreferencesKey(”keyname“)
代表Set<String>
型
看着很简单,不要怕,就是把SharedPreferences
的key
通过上面对应数据类型的函数转换成Preferences.Key
,看不明白的话我们就往下看看实际使用方法吧,GOGOGO ~
一、DataStore新手配置
1.引入库
implementation("androidx.datastore:datastore-preferences:1.0.0")
2.初始化
新建一个PreferencesRepository.kt文件
val Context.settings: DataStore<Preferences> by preferencesDataStore("settings")
class PreferencesRepository(context: Context) {
val settings = context.settings
}
我来解释一下下,我创建了一个PreferencesRepository
类,在class外层创建一个Context.settings
扩展变量是文件名为settings
的DataStore
的实例,这样有个好处可以在任意一个有Context
的地方拿到DataStore
同一个实例,避免触发同一个文件被多个地方初始化的错误
二、DataStore通过Key读取和写入Value
val Context.settings: DataStore<Preferences> by preferencesDataStore("settings")
class PreferencesRepository(context: Context) {
//声明所有要用的KEY
private object PreferencesKeys {
val USER_NAME = stringPreferencesKey("username")
}
//从Context获取settings
private val settings = context.settings
/**
* 写入
*/
suspend fun setUserName(text: String) {
settings.edit {
it[PreferencesKeys.USER_NAME] = text
}
}
/**
* 读取
*/
fun getUserNameFlow(): Flow<String> {
return settings.data.map {
it[PreferencesKeys.USER_NAME] ?: "啥也不是"
}
}
}
DataStore
的读取和写入都需要用到协程,你也可以Android
官网的用RxJava
使用DataStore
,我是不习惯RxJava
就直接用kotlin
来写了,这只是一个简单的示例,下面我就来更详细的演示一下实际应用
三、DataStore实战示例
1.把DataStore转成数据类Settings
val Context.settings: DataStore<Preferences> by preferencesDataStore("settings")
data class Settings(val username: String, val themes: String)
class PreferencesRepository(context: Context) {
//声明所有要用的KEY
private object PreferencesKeys {
val USER_NAME = stringPreferencesKey("username")
val THEMES = stringPreferencesKey("themes")
}
//从Context获取settings
private val settings = context.settings
//把settings数据整合转成数据类
val settingsFlow = settings.data
.catch { exception ->
if (exception is IOException) {
//如果读取DataStore文件发生IO异常就返回一个空文件
emptyPreferences()
} else {
//如果是其他异常就抛出异常
throw exception
}
}.map {
//把DataStore文件内容转成流Flow<Settings>,方便转成LiveData
val username = it[PreferencesKeys.USER_NAME] ?: ""
val themes = it[PreferencesKeys.THEMES] ?: "0"
Settings(username, themes)
}
/**
* 写入
*/
suspend fun setUserName(text: String) {
settings.edit {
it[PreferencesKeys.USER_NAME] = text
}
}
/**
* 读取
*/
fun getUserNameFlow(): Flow<String> {
return settings.data.map {
it[PreferencesKeys.USER_NAME] ?: "啥也不是"
}
}
}
2.创建ViewModel并使用PreferencesRepository
class PreferencesViewModel(context: Context) : ViewModel() {
private val repository = PreferencesRepository(context)
private val settingsFlow = repository.settingsFlow
//把settingsFlow转成LiveData
val settings = settingsFlow.asLiveData()
fun setUserName(text: String) {
viewModelScope.launch {
repository.setUserName(text)
}
}
//直接获取
fun getUserName(): String {
return runBlocking {
repository.getUserNameFlow().first()
}
}
//通过LiveData观察UserName变化
fun getUserNameLiveData(): LiveData<String> {
return repository.getUserNameFlow().asLiveData()
}
class Factory(
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.在Activity使用PreferencesViewModel
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]
//直接获取username
val userName = viewModel.getUserName()
Log.d("TestActivity", "getUserName: $userName")
//通过LiveData观察userName变化
viewModel.getUserNameLiveData().observe(this) { value ->
Log.d("TestActivity", "observe userName: $value")
}
//通过LiveData观察settings数据类变化
viewModel.settings.observe(this) {
Log.d("TestActivity", "observe settings: $it")
}
//快试试设置用户名吧
viewModel.setUserName("Felix")
binding.save.setOnClickListener {
//保存输入用户名
viewModel.setUserName(binding.username.text.toString())
}
}
}
布局部分
<?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="保存" />
</LinearLayout>
</FrameLayout>
4.显示效果
四、PreferenceFragmentCompat根据Datastore数据显示UI
1.引入库
implementation("androidx.preference:preference:1.2.1")
2.创建PreferenceFragmentCompat布局
在项目app/main/res/layout文件夹下创建一个fragment_settings_prefernces.xml
<?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>
在项目app/main/res/values文件夹下创建一个preferences_arrays.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="themes_labels">
<item>默认</item>
<item>圣罗兰紫</item>
<item>皮卡丘黄</item>
<item>超活力橙</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>
在项目app/main/res/xml文件夹下创建settings_preferences.xml
<?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="主题设置" />
<EditTextPreference
android:key="username"
app:summary="好像一般这个也不会出现在设置里面,哈哈哈,我这是示例,仅供参考"
app:title="用户名" />
</PreferenceScreen>
3.创建PreferenceDataStoreAdapter(关键)
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.创建SettingsPreferencesFragment并使用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 {
//自定义PreferenceFragmentCompat布局
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.修改TestActivity增加设置Button跳转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="保存" />
<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="设置" />
</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]
//直接获取username
val userName = viewModel.getUserName()
Log.d("TestActivity", "getUserName: $userName")
//通过LiveData观察userName变化
viewModel.getUserNameLiveData().observe(this) { value ->
Log.d("TestActivity", "observe userName: $value")
binding.username.setText(value)
}
//通过LiveData观察settings数据类变化
viewModel.settings.observe(this) {
Log.d("TestActivity", "observe settings: $it")
}
//快试试设置用户名吧
viewModel.setUserName("Felix")
binding.save.setOnClickListener {
//保存输入用户名
viewModel.setUserName(binding.username.text.toString())
}
binding.settings.setOnClickListener {
supportFragmentManager.beginTransaction()
.add(
R.id.fragment_content,
SettingsPreferencesFragment(),
SettingsPreferencesFragment::class.simpleName
)
.commitAllowingStateLoss()
}
}
}
6.显示效果
总结
看起来好像很麻烦,实际上还好,只是我为了写详细一点,贴了很多代码出来。DataStore
配合ViewModel
比SharedPreferences
方便,DataStore采用异步方式读取和写入避免了ANR
(虽然我没碰到过,官方这么说的),在写这篇文章之前我一直卡在PreferenceFragmentCompat
显示,因为PreferenceDataStore
提供的是方法不直接支持协程,看了Android资料之后才知道可以用runBlocking
在非协程上下文函数中执行挂起函数,但是注意的是我们必须使用first()
函数不能使用last()
,避免造成ANR
,我这里就不详细讲了,不想把这些都写在一篇文章里面,怕到时候看太乱了,想了解详细情况可以去查资料。
最后在使用中还遇到一个问题:
PreferenceFragmentCompat
布局中的ListPreference
控件不支持存储int,这是官方控件的缺陷,我已经提交报告给官方了看后续,如果要现在解决这个问题有两个办法,第一个是把本来要存储int的改成存储字符串,第二个是自定义一个ListPreference
控件,问题就是ListPreference
中把entryValues
属性读取成CharSequence[] mEntryValues
,这样无论我们传什么数组都会变成字符串数组,存值的时候就会存成字符串,这就是问题所在。
我在了一篇文章提出了这个解决办法《Android解决PreferenceFragmentCompat不能保存Int的缺陷》
不要在在意`ui`,主打一个`DataStore`使用有如何问题或有更好的想法,请在下方留言,互相学习