NeoPreference延伸:为SharedPreferences配置项生成配置页面

news2025/6/23 6:53:08

代码地址:https://github.com/Nagi1225/NeoPreference.git

最初在开发NeoPreference这个SharedPreferences工具的时候,就期望完成三个目标:

  1. 代码简洁,新增配置项的时候一行代码(最多两行);
  2. 读写安全,包括数据类型安全,支持类型的进一步修饰,例如,可以指定整数范围;
  3. 可以自动生成配置页,新增配置项的时候不需要手动去页面上添加。

前两个目标已经完成,参见SharedPreferences的一种极简优雅且安全的用法 和 NeoPreference:一个简化SharedPreferences使用的工具

第三个目标是考虑到那些配置项可能对应用户偏好设置的情况,这样新增配置就不需要去修改页面,新增配置项的时候,页面就会自动补充;另外,也可以用于生成调试页面,不需要针对SharedPreferences再单独写调试页面。

本文针对第三个目标给出一个方案。(暂时仅支持int、float等基本类型的配置项)

Config配置示例

@Config.Name(DemoConfig.NAME)
public interface DemoConfig extends Config {
    String NAME = "demo_config";

    @IntItem(key = "app_open_count", description = "应用打开次数")
    Property<Integer> intProperty();

    @StringItem(key = "user_id", description = "用户id")
    Property<String> stringProperty();

    @FloatItem(key = "height", description = "xx高度")
    Property<Float> floatProperty();

    @LongItem(key = "last_save_time", description = "上一次保存时间")
    Property<Long> longProperty();

    @BooleanItem(key = "is_first_open", defaultValue = true, description = "应用是否第一次启动")
    Property<Boolean> boolProperty();

    @StringSetItem(key = "collection_media_set", valueOf = {"mp3", "mp4", "png", "jpg", "mkv"})
    Property<Set<String>> collectMediaSet();

    @JsonData.JsonItem(key = "current_user_info")
    Property<UserInfo> userInfo();
}

这里为键值对指明描述信息,便于页面展示。

页面实现代码

代码较长,可以先跳到后面看显示效果。(布局等信息,见代码仓库完整实现)

public class AutoConfigActivity extends AppCompatActivity {
    public static final String ARG_CONFIG_CLASS = "config_class";
    private static final int OBJECT_TYPE = 0;
    private static final int INTEGER_TYPE = 1;
    private static final int FLOAT_TYPE = 2;
    private static final int STRING_TYPE = 3;
    private static final int BOOLEAN_TYPE = 4;
    private static final int LONG_TYPE = 5;

    public static void start(Activity activity, Class<?> configClass) {
        Intent intent = new Intent(activity, AutoConfigActivity.class);
        intent.putExtra(ARG_CONFIG_CLASS, configClass);
        activity.startActivity(intent);
    }

    private final List<Property<?>> propertyList = new ArrayList<>();


    private final RecyclerView.Adapter<ConfigItemHolder> adapter = new RecyclerView.Adapter<>() {
        @NonNull
        @Override
        public ConfigItemHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            switch (viewType) {
                case INTEGER_TYPE:
                    return new IntegerItemHolder(parent);
                case FLOAT_TYPE:
                    return new FloatItemHolder(parent);
                case LONG_TYPE:
                    return new LongItemHolder(parent);
                case BOOLEAN_TYPE:
                    return new BooleanItemHolder(parent);
                case STRING_TYPE:
                    return new StringItemHolder(parent);
                case OBJECT_TYPE:
                    return new ObjectItemHolder(parent);
                default:
                    return null;
            }
        }

        @Override
        public void onBindViewHolder(@NonNull ConfigItemHolder holder, int position) {
            holder.setData(propertyList.get(position));
        }

        @Override
        public int getItemCount() {
            return propertyList.size();
        }

        @Override
        public int getItemViewType(int position) {
            Class<?> valueClass = propertyList.get(position).getValueClass();
            if (valueClass.equals(Integer.class)) {
                return INTEGER_TYPE;
            } else if (valueClass.equals(Float.class)) {
                return FLOAT_TYPE;
            } else if (valueClass.equals(Long.class)) {
                return LONG_TYPE;
            } else if (valueClass.equals(Boolean.class)) {
                return BOOLEAN_TYPE;
            } else if (valueClass.equals(String.class)) {
                return STRING_TYPE;
            } else {
                return OBJECT_TYPE;
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityAutoConfigBinding binding = ActivityAutoConfigBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        binding.rvConfigList.setHasFixedSize(true);
        binding.rvConfigList.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
        binding.rvConfigList.setLayoutManager(new LinearLayoutManager(this));
        binding.rvConfigList.setAdapter(adapter);

        Class<? extends Config> configClass = (Class<? extends Config>) getIntent().getSerializableExtra(ARG_CONFIG_CLASS);
        Config config = ConfigManager.getInstance().getConfig(configClass);
        propertyList.addAll(config.getAll());
        adapter.notifyItemRangeInserted(0, propertyList.size());

        for (int i = 0; i < propertyList.size(); i++) {
            int index = i;
            propertyList.get(i).addListener(this, s -> adapter.notifyItemChanged(index));
        }
    }

    static abstract class ConfigItemHolder<T> extends RecyclerView.ViewHolder {
        final HolderConfigPropertyBinding binding;

        public ConfigItemHolder(@NonNull HolderConfigPropertyBinding binding) {
            super(binding.getRoot());
            this.binding = binding;
        }

        void setData(Property<T> property) {
            if (TextUtils.isEmpty(property.getDescription())) {
                binding.tvPropertyName.setText(property.getKey());
            } else {
                binding.tvPropertyName.setText(property.getKey() + "(" + property.getDescription() + ")");
            }

            binding.tvPropertyValue.setText(property.getValueString());
        }
    }

    static class IntegerItemHolder extends ConfigItemHolder<Integer> {

        public IntegerItemHolder(@NonNull ViewGroup parent) {
            super(HolderConfigPropertyBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
        }

        @Override
        void setData(Property<Integer> property) {
            super.setData(property);

            binding.btnEdit.setOnClickListener(v -> {
                DialogInputBinding dialogBinding = DialogInputBinding.inflate(LayoutInflater.from(itemView.getContext()));
                AlertDialog alertDialog = new AlertDialog.Builder(itemView.getContext())
                        .setTitle("Set " + property.getKey())
                        .setView(dialogBinding.getRoot())
                        .setPositiveButton("save", (dialog, which) -> property.set(Integer.parseInt(dialogBinding.etInput.getText().toString())))
                        .create();
                alertDialog.show();

                Button button = alertDialog.getButton(DialogInterface.BUTTON_POSITIVE);
                dialogBinding.etInput.setHint("Please input a integer");
                dialogBinding.etInput.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED);
                dialogBinding.etInput.addTextChangedListener(onTextChanged(s -> button.setEnabled(!TextUtils.isEmpty(s))));
                dialogBinding.etInput.setText(property.exists() ? String.valueOf(property.get()) : "");
            });
        }
    }

    static class FloatItemHolder extends ConfigItemHolder<Float> {

        public FloatItemHolder(@NonNull ViewGroup parent) {
            super(HolderConfigPropertyBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
        }

        @Override
        void setData(Property<Float> property) {
            super.setData(property);
            binding.btnEdit.setOnClickListener(v -> {
                DialogInputBinding dialogBinding = DialogInputBinding.inflate(LayoutInflater.from(itemView.getContext()));
                AlertDialog alertDialog = new AlertDialog.Builder(itemView.getContext())
                        .setTitle("Set " + property.getKey())
                        .setView(dialogBinding.getRoot())
                        .setPositiveButton("save", (dialog, which) -> property.set(Float.parseFloat(dialogBinding.etInput.getText().toString())))
                        .create();
                alertDialog.show();

                Button button = alertDialog.getButton(DialogInterface.BUTTON_POSITIVE);
                dialogBinding.etInput.setHint("Please input a float");
                dialogBinding.etInput.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
                dialogBinding.etInput.addTextChangedListener(onTextChanged(s -> button.setEnabled(!TextUtils.isEmpty(s))));
                dialogBinding.etInput.setText(property.exists() ? String.valueOf(property.get()) : "");
            });
        }
    }

    static class BooleanItemHolder extends ConfigItemHolder<Boolean> {

        public BooleanItemHolder(@NonNull ViewGroup parent) {
            super(HolderConfigPropertyBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
        }

        @Override
        void setData(Property<Boolean> property) {
            super.setData(property);
            binding.btnEdit.setOnClickListener(v -> {
                AtomicBoolean value = new AtomicBoolean(property.get(false));
                AlertDialog alertDialog = new AlertDialog.Builder(itemView.getContext())
                        .setTitle("Set " + property.getKey())
                        .setSingleChoiceItems(new CharSequence[]{"true", "false"}, value.get() ? 0 : 1, (dialog, which) -> value.set(which == 0))
                        .setPositiveButton("save", (dialog, which) -> property.set(value.get()))
                        .create();
                alertDialog.show();
            });
        }
    }

    static class LongItemHolder extends ConfigItemHolder<Long> {

        public LongItemHolder(@NonNull ViewGroup parent) {
            super(HolderConfigPropertyBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
        }

        @Override
        void setData(Property<Long> property) {
            super.setData(property);
            binding.btnEdit.setOnClickListener(v -> {
                DialogInputBinding dialogBinding = DialogInputBinding.inflate(LayoutInflater.from(itemView.getContext()));
                AlertDialog alertDialog = new AlertDialog.Builder(itemView.getContext())
                        .setTitle("Set " + property.getKey())
                        .setView(dialogBinding.getRoot())
                        .setPositiveButton("save", (dialog, which) -> property.set(Long.parseLong(dialogBinding.etInput.getText().toString())))
                        .create();
                alertDialog.show();

                Button button = alertDialog.getButton(DialogInterface.BUTTON_POSITIVE);
                dialogBinding.etInput.setHint("Please input a long");
                dialogBinding.etInput.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED);
                dialogBinding.etInput.addTextChangedListener(onTextChanged(s -> button.setEnabled(!TextUtils.isEmpty(s))));
                dialogBinding.etInput.setText(property.exists() ? String.valueOf(property.get()) : "");
            });
        }
    }

    static class StringItemHolder extends ConfigItemHolder<String> {

        public StringItemHolder(@NonNull ViewGroup parent) {
            super(HolderConfigPropertyBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
        }

        @Override
        void setData(Property<String> property) {
            super.setData(property);
            binding.btnEdit.setOnClickListener(v -> {
                DialogInputBinding dialogBinding = DialogInputBinding.inflate(LayoutInflater.from(itemView.getContext()));
                AlertDialog alertDialog = new AlertDialog.Builder(itemView.getContext())
                        .setTitle("Set " + property.getKey())
                        .setView(dialogBinding.getRoot())
                        .setPositiveButton("save", (dialog, which) -> property.set(dialogBinding.etInput.getText().toString()))
                        .create();
                alertDialog.show();

                Button button = alertDialog.getButton(DialogInterface.BUTTON_POSITIVE);
                dialogBinding.etInput.setHint("Please input a string");
                dialogBinding.etInput.addTextChangedListener(onTextChanged(s -> button.setEnabled(!TextUtils.isEmpty(s))));
                dialogBinding.etInput.setText(property.exists() ? String.valueOf(property.get()) : "");
            });
        }
    }

    static class ObjectItemHolder extends ConfigItemHolder<Object> {

        public ObjectItemHolder(@NonNull ViewGroup parent) {
            super(HolderConfigPropertyBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
        }

        @Override
        void setData(Property<Object> property) {
            super.setData(property);
            binding.btnEdit.setVisibility(View.GONE);
        }
    }

    static TextWatcher onTextChanged(Consumer<CharSequence> listener) {
        return new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {

            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                listener.accept(s);
            }

            @Override
            public void afterTextChanged(Editable s) {

            }
        };
    }
}

页面显示效果

  1. 根据配置项自动生成的页面:

在这里插入图片描述

  1. 配置项对应的编辑弹窗:

在这里插入图片描述)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1254006.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

顶级安卓数据恢复工具—— 15 个 Android 数据恢复程序榜单

探索并比较顶级 Android 数据恢复软件&#xff0c;并选择最好的 Android 恢复应用程序来恢复您的宝贵数据&#xff1a; 特别是您的智能手机或 Android 设备可以完成许多繁重的工作&#xff0c;其中最有用的是存储数据。Android 设备可以伪装成照片、视频、电子邮件甚至敏感商业…

YOLOv5轻量化改进之MobileNetv3

目录 一、原理 二、代码 三、应用到YOLOv5 一、原理 我们提出了基于互补搜索技术和新颖架构设计相结合的下一代mobilenet。MobileNetV3通过硬件网络架构搜索(NAS)和NetAdapt算法的结合来调整到移动电话cpu,然后通过新的架构进步进行改进。本文开始探索自动搜索算法和网络设计…

图片处理工具JixiPix Pastello mac中文版功能特色

JixiPix Pastello mac是一款数字绘画软件&#xff0c;它可以将照片转换为仿佛是手绘的油画、粉笔画、素描等风格的艺术作品。该软件提供了多种绘画效果和工具&#xff0c;例如颜料、画笔、纸张等&#xff0c;让用户可以轻松地调整画作的亮度、色彩和细节等参数&#xff0c;从而…

访谈 破风之人毛京波,选择难而正确的路

“无论是在燃油时代还是电动时代&#xff0c;我们所做的一切&#xff0c;只为回归纯粹的驾驶乐趣。”履新路特斯中国总裁整整一年的毛京波&#xff0c;从不放过任何一个展示路特斯品牌驾驭精神的机会。 11月17日&#xff0c;广州车展开幕首日&#xff0c;位于5.2馆的路特斯“冠…

flex布局实战之自动填充剩余

案例目标 文字部分自适应并且居中 图中是一个弹窗&#xff0c;我现在使用flex的布局来实现&#xff0c;标题和关闭按钮。因为是uni-app,所以标签是view 。你可以自行替换为 代码 <view class"popup-box"><view class"title"><view class&…

【Kotlin】类与接口

文章目录 类的定义创建类的实例构造函数主构造函数次构造函数init语句块 数据类的定义数据类定义了componentN方法 继承AnyAny&#xff1a;非空类型的根类型Any?&#xff1a;所有类型的根类型 覆盖方法覆盖属性覆盖 抽象类接口:使用interface关键字函数&#xff1a;funUnit:让…

【Kotlin】引入与基础语法

文章目录 Kotlin的特性Kotlin优势Kotlin的安卓项目变量变量保存了指向对象的引用优先使用val来避免副作用 后端变量Backing Fields延迟初始化 Kotlin的特性 它更加易表现&#xff1a;这是它最重要的优点之一。你可以编写少得多的代码。Kotlin是一种兼容Java的语言Kotlin比Java…

企业计算机服务器中了mkp勒索病毒怎么办?Mkp勒索病毒解密数据恢复

网络技术的不断发展&#xff0c;为企业的生产运营提供了坚实的基础&#xff0c;但随之而来的网络安全威胁也不断增加&#xff0c;影响了企业的正常生产生活。近期&#xff0c;云天数据恢复中心陆续接到很多企业的求助&#xff0c;企业计算机服务器遭到了mkp勒索病毒攻击&#x…

不同品牌的手机可以则哪一个你投屏到电视?

如果你使用AirDroid Cast的TV版&#xff0c;苹果手机可以通过airPlay或无线投屏方式&#xff0c;将屏幕同步到电视屏幕&#xff1b;多个品牌的安卓手机可以通过无线投屏投射到电视。而且无线投屏不限制距离&#xff0c;即使是远程投屏也可以实现。 打开AirDroid Cast的TV版&…

External model DLL ”ADC083XDLL“ not found_proteus仿真报错解决方法

仿真运行报错 External model DLL ”ADC083XDLL“ not found 原因 是proteus仿真软件缺少ADC083X.DLL文件或者ADC083X.DLL文件损坏。 解决方法 1.下载没问题的ADC083x.DLL ADC083X.DLL下载链接&#xff1a; 2.找到库文件夹&#xff0c;替换库文件ADC083X.DLL 库文件夹位置…

快速、精确仿真高频电磁场的工具CST Studio Suite 2024版本下载与安装配置

目录 前言一、CST 2024 安装二、使用配置总结 前言 CST Studio Suite是一个集成的仿真工具套件&#xff0c;用于模拟和优化电子系统的性能。它包括多个工具和模块&#xff0c;如电磁仿真、结构仿真、热仿真、电路分析等&#xff0c;以支持从概念设计到生产部署的整个开发周期。…

《微信小程序开发从入门到实战》学习三十一

3.4 开发参与投票页面 3.4.9 显示投票结果 在实际使用中&#xff0c;一个用户不能对同一个投票进行重复提交&#xff0c;因此需要向服务器端提交投票结果和提交用户ID。另外页面&#xff0c;需要完善。用户提交完投票后 &#xff0c;还需要显示投票目前的结果&#xff0c;提交…

3-全功能pytest测试框架-allure2

3-全功能pytest测试框架-allure2 一:Allure介绍1> Allure 优势2> Allure 安装二:allure demo1> 脚本demo2> 查看result三:allure装饰器1> 装饰器一览表2> 装饰器概述1. epic、feature、story3> @allure.title()4> @allure.testcase()5> @allure.…

【JMeter】不同场景下的接口请求

场景1: 上传文件接口即Content-Type=multipart/form-data 步骤: 1. 接口url,method以及path正常填写 2.文件上传content-type是multipart/form-data,所以可以勾选【use multipart/form-data】,如果还有其他请求头信息可以添加一个请求头元件 3.请求参…

来自Microsoft Teams的摄像头背景图片

原文件在&#x1f446;&#xff0c;下面是预览图 如果你安装了Microsoft Teams也可以搜索MSTeams&#xff0c;就在MSTeams/Backgrounds

处理分类问题的不平衡数据的 5 种技术

一、介绍 分类问题在机器学习领域很常见。正如我们所知&#xff0c;在分类问题中&#xff0c;我们试图通过研究输入数据或预测变量来预测类标签&#xff0c;其中目标或输出变量本质上是分类变量。 如果您已经处理过分类问题&#xff0c;那么您一定遇到过以下情况&#xff1a;其…

贝叶斯个性化排序损失函数

贝叶斯个性化排名&#xff08;Bayesian Personalized Ranking, BPR&#xff09;是一种用于推荐系统的机器学习方法&#xff0c;旨在为用户提供个性化的排名列表。BPR的核心思想是通过对用户历史行为数据的分析&#xff0c;对用户可能喜欢和不喜欢的物品对&#xff08;item pair…

【matlab程序】matlab画台风符号和实例应用

【matlab程序】matlab画台风符号和实例应用 没有看文献&#xff0c;不知道文献中的符号什么样子&#xff0c;据我理解为这样子的&#xff1a; 因此&#xff0c;按照自己的理解做了这期。 结果浏览&#xff1a; 台风符号一切可改&#xff0c;可细细改。可是我不发论文&#xf…

3、MSF使用

文章目录 一、利用ms17-010漏洞对靶机执行溢出攻击二、后渗透模块meterpreter的使用 一、利用ms17-010漏洞对靶机执行溢出攻击 分别输入以下命令&#xff0c;使用ms17_010_eternalblue模块对目标机的ms17-010漏洞进行利用&#xff1a; use exploit/windows/smb/ms17_010_eter…

Percepio Tracealyzer 4.8.1 视觉跟踪诊断解决方案

Percepio Tracealyzer 4.8.1 视觉跟踪诊断解决方案&#xff0c; 是使嵌入式软件开发人员能够深入了解其运行时系统。这样可以更轻松地调试系统级问题、查找软件设计缺陷以及测量软件时序和资源使用情况。确保您的代码可靠、高效且响应迅速。 视觉运行时洞察 在运行时将 X 射线视…