Android 首选项PreferenceFragmentCompat之Jetpack DataStore

Jetpack 系列文章


这是Jetpack 系列文章,还有好多没时间写,反正有时间我就记录一点,有什么问题留言后看到会回复!🙂


前言

这篇主要讲的是用DataStore取代SharedPreferences,并且可使用PreferenceFragmentCompat快速配置UI修改首选项。在这里容我先说两句废话,DataStore我在很久之前就注意到了,看上去与SharedPreferences使用还是有点区别的,不过用上了就觉得还好,就是使用kotlinFlow需要配合协程使用,手上之前的项目都是java写的也都是使用SharedPreferences,最近来升级项目想着刚好把SharedPreferences改成DataStore,不用不知道一用就乱了(liao),明显感觉比SharedPreferences复杂一点,我来说说我遇到的几个问题吧

  • DataStore如何像SharedPreferences通过Key编辑和获取Value?
  • DataStore不支持创建多个实例?
  • DataStore怎么监听Value变化?
  • DataStore如何使用PreferenceFragmentCompat读取和修改首选项值?

了解DataStore和SharedPreferences的Key区别

DataStoreKeyPreferences.Key<*>SharedPreferenceskeyString,支持的存储数据类型都一样,只不过过呢官方把DataStoreKey升级了一下,把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>

看着很简单,不要怕,就是把SharedPreferenceskey通过上面对应数据类型的函数转换成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扩展变量是文件名为settingsDataStore的实例,这样有个好处可以在任意一个有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.显示效果

Felix DataStore储存测试

四、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配合ViewModelSharedPreferences方便,DataStore采用异步方式读取和写入避免了ANR(虽然我没碰到过,官方这么说的),在写这篇文章之前我一直卡在PreferenceFragmentCompat显示,因为PreferenceDataStore提供的是方法不直接支持协程,看了Android资料之后才知道可以用runBlocking在非协程上下文函数中执行挂起函数,但是注意的是我们必须使用first()函数不能使用last(),避免造成ANR,我这里就不详细讲了,不想把这些都写在一篇文章里面,怕到时候看太乱了,想了解详细情况可以去查资料。

最后在使用中还遇到一个问题:

PreferenceFragmentCompat布局中的ListPreference控件不支持存储int,这是官方控件的缺陷,我已经提交报告给官方了看后续,如果要现在解决这个问题有两个办法,第一个是把本来要存储int的改成存储字符串,第二个是自定义一个ListPreference控件,问题就是ListPreference中把entryValues属性读取成CharSequence[] mEntryValues,这样无论我们传什么数组都会变成字符串数组,存值的时候就会存成字符串,这就是问题所在。

我在了一篇文章提出了这个解决办法《Android解决PreferenceFragmentCompat不能保存Int的缺陷

不要在在意`ui`,主打一个`DataStore`使用

有如何问题或有更好的想法,请在下方留言,互相学习

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Felix_Fly2021

你的鼓励是我创作最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值