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

代码地址: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. 配置项对应的编辑弹窗:

在这里插入图片描述)

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

线程的常用方法-wait和notify以及线程的结束方式

再复习一下Java中的线程的状态图 wait和sleep的区别是&#xff1a;wait需要先持有锁&#xff08;wait需要再synchronized代码块中执行&#xff09;&#xff0c;执行后会让出锁。而sleep不需要先持有锁&#xff0c;执行后也不会释放锁&#xff08;有锁的话抱着锁睡觉&#xff09…

SpringBoot 环境使用 Redis + AOP + 自定义注解实现接口幂等性

目录 一、前言二、主流实现方案介绍2.1、前端按钮做加载状态限制&#xff08;必备&#xff09;2.2、客户端使用唯一标识符2.3、服务端通过检测请求参数进行幂等校验&#xff08;本文使用&#xff09; 三、代码实现3.1、POM3.2、application.yml3.3、Redis配置类3.4、自定义注解…

基于Haclon的标签旋转项目案例

项目要求&#xff1a; 图为HALCON附图“25interleaved_exposure_04”&#xff0c;里面为旋转的二维码标签&#xff0c;请将其旋转到水平位置。 项目知识&#xff1a; 在HALCON中进行图像平移和旋转通常有以下步骤&#xff1a; &#xff08;1&#xff09;通过hom_mat2d_ident…

jQuery_03 dom对象和jQuery对象的互相转换

dom对象和jQuery对象 dom对象 jQuery对象 在一个文件中同时存在两种对象 dom对象: 通过js中的document对象获取的对象 或者创建的对象 jQuery对象: 通过jQuery中的函数获取的对象。 为什么使用dom或jQuery对象呢&#xff1f; 目的是 要使用dom对象的函数或者属性 以及呢 要…

<JavaEE> 线程的五种创建方法 和 查看线程的两种方式

目录 一、线程的创建方法 1.1 继承 Thread -> 重写 run 方法 1.2 使用匿名内部类 -> 继承 Thread -> 重写 run 方法 1.3 实现 Runnable 接口 -> 重写 run 方法 1.4 使用匿名内部类 -> 实现 Runnable 接口 -> 重写 run 方法 1.5 使用 lambda 表达式 二…

Self Distillation 自蒸馏论文解读

paper&#xff1a;Be Your Own Teacher: Improve the Performance of Convolutional Neural Networks via Self Distillation official implementation&#xff1a; https://github.com/luanyunteng/pytorch-be-your-own-teacher 前言 知识蒸馏作为一种流行的压缩方法&#…

五种多目标优化算法(MOGWO、MOLPB、MOJS、NSGA3、MOPSO)求解微电网多目标优化调度(MATLAB代码)

一、多目标优化算法简介 &#xff08;1&#xff09;多目标灰狼优化算法MOGWO 多目标应用&#xff1a;基于多目标灰狼优化算法MOGWO求解微电网多目标优化调度&#xff08;MATLAB代码&#xff09;-CSDN博客 &#xff08;2&#xff09;多目标学习者行为优化算法MOLPB 多目标学习…

ps5ps4游戏室如何计时?计费系统怎么查看游戏时间以及收费如何管理

ps5ps4游戏室如何计时&#xff1f;计费系统怎么查看游戏时间以及收费如何管理 1、ps5ps4游戏室如何计时&#xff1f; 下图以佳易王计时计费软件V17.9为例说明 在开始计时的时候&#xff0c;只需点 开始计时按钮&#xff0c;那么开台时间和使用的时间长度项目显示在屏幕上&am…

如何判断一个题目用“贪心/动态规划“还是用“BFS/DFS”方法解决

1 总结 1.1 贪心、动态规划和BFS/DFS题解的关系 一般能使用贪心、动态规划解决一个问题时&#xff0c;使用BFS&#xff0c;DFS也能解决这个题&#xff0c;但是反之不能成立。 1.2 2 贪心 -> BFS/DFS 2.1 跳跃游戏1和3的异同 这两道题&#xff0c;“跳跃游戏”&#xf…

靡靡之音 天籁之声 ——Adobe Audition

上一期讲到了和Pr配合使用的字幕插件Arctime Pro的相关介绍。相信还记得的小伙伴应该记得我还提到过一个软件叫做Au。 当人们对字幕需求的逐渐满足&#xff0c;我们便开始追求更高层次的享受&#xff0c;当视觉享受在进步&#xff0c;听觉享受想必也不能被落下&#xff01; Au即…

Flutter桌面应用开发之毛玻璃效果

目录 效果实现方案依赖库支持平台实现步骤注意事项话题扩展 毛玻璃效果&#xff1a;毛玻璃效果是一种模糊化的视觉效果&#xff0c;常用于图像处理和界面设计中。它可以通过在图像或界面元素上应用高斯模糊来实现。使用毛玻璃效果可以增加图像或界面元素的柔和感&#xff0c;同…

一、深入简出串口(USRT)通信——基本概念。

一、前言 串口到底是什么&#xff1f;简单来说一句话就可以解释&#xff0c;串口就是一种通信协议。 看到这里可能大家会觉得你这不是放屁么&#xff0c;说了跟没说一样。所以这里做前言来描述&#xff0c;大家要先对通信协议有一个下意识地认识才能在学习串口的时候不至于迷茫…

spring循环依赖

Bean的生命周期 这里不会对Bean的生命周期进行详细的描述&#xff0c;只描述一下大概的过程。 Bean的生命周期指的就是&#xff1a;在Spring中&#xff0c;Bean是如何生成的&#xff1f; 被Spring管理的对象叫做Bean。Bean的生成步骤如下&#xff1a; Spring扫描class得到Bean…

yolo系列中的一些评价指标说明

文章目录 一. 混淆矩阵二. 准确度(Accuracy)三. 精确度(Precision)四. 召回率(Recall)五. F1-score六. P-R曲线七. AP八. mAP九. mAP0.5十. mAP[0.5:0.95] 一. 混淆矩阵 TP (True positives)&#xff1a;被正确地划分为正例的个数&#xff0c;即实际为正例且被分类器划分为正例…

计算机编程基础教程,中文编程工具下载,编程构件组合按钮

计算机编程基础教程&#xff0c;中文编程工具下载&#xff0c;编程构件组合按钮 给大家分享一款中文编程工具&#xff0c;零基础轻松学编程&#xff0c;不需英语基础&#xff0c;编程工具可下载。 这款工具不但可以连接部分硬件&#xff0c;而且可以开发大型的软件&#xff0c…

人力资源管理后台 === 登陆+主页灵鉴权

目录 1. 分析登录流程 2. Vuex中用户模块的实现 3.Vue-cli代理解决跨域 4.axios封装 5.环境区分 6. 登录联调 7.主页权限验证-鉴权 1. 分析登录流程 传统思路都是登录校验通过之后&#xff0c;直接调用接口&#xff0c;获取token之后&#xff0c;跳转到主页。 vue-elemen…

C++二分查找:统计点对的数目

本题其它解法 C双指针算法&#xff1a;统计点对的数目 本周推荐阅读 C二分算法&#xff1a;得到子序列的最少操作次数 本文涉及的基础知识点 二分查找算法合集 题目 给你一个无向图&#xff0c;无向图由整数 n &#xff0c;表示图中节点的数目&#xff0c;和 edges 组成…

HTTP状态码:如何修复 404 Not Found错误?

互联网上各种类型的网站非常多&#xff0c;无论用户还是网站运营者不可避免的会遇到404 Not Found错误&#xff0c;如果遇到404错误&#xff0c;我们应该如何解决呢&#xff1f; 对于用户 检查拼写错误 如果您是遇到错误的用户&#xff0c;请仔细检查 URL 是否有任何拼写错误…

【Flutter 常见问题系列 第 1 篇】Text组件 文字的对齐、数字和字母对齐中文

TextStyle中设置height参数即可 对齐的效果 Text的高度 是根据 height 乘于 fontSize 进行计算的、这里指定heiht即可、不指定的会出现 无法对齐的情况&#xff0c;如下&#xff1a; 这种就是无法对齐的情况

决策树(第四周)

一、决策树基本原理 如下图所示&#xff0c;是一个用来辨别是否是猫的二分类器。输入值有三个&#xff08;x1&#xff0c;x2&#xff0c;x3&#xff09;&#xff08;耳朵形状&#xff0c;脸形状&#xff0c;胡须&#xff09;&#xff0c;其中x1{尖的&#xff0c;圆的}&#xf…