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@Overridepublic 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;}}@Overridepublic void onBindViewHolder(@NonNull ConfigItemHolder holder, int position) {holder.setData(propertyList.get(position));}@Overridepublic int getItemCount() {return propertyList.size();}@Overridepublic 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;}}};@Overrideprotected 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));}@Overridevoid 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));}@Overridevoid 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));}@Overridevoid 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));}@Overridevoid 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));}@Overridevoid 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));}@Overridevoid setData(Property<Object> property) {super.setData(property);binding.btnEdit.setVisibility(View.GONE);}}static TextWatcher onTextChanged(Consumer<CharSequence> listener) {return new TextWatcher() {@Overridepublic void beforeTextChanged(CharSequence s, int start, int count, int after) {}@Overridepublic void onTextChanged(CharSequence s, int start, int before, int count) {listener.accept(s);}@Overridepublic void afterTextChanged(Editable s) {}};}
}

页面显示效果

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

在这里插入图片描述

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

在这里插入图片描述)

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

蓝桥杯第十四届蓝桥杯模拟赛第三期考场应对攻略(C/C++)

这里把我的想法和思路写出来&#xff0c;恳请批评指正&#xff01; 目录 考前准备 试题1&#xff1a; 试题2&#xff1a; 试题3&#xff1a; 试题4&#xff1a; 试题5&#xff1a; 试题6&#xff1a; 试题7&#xff1a; 试题8&#xff1a; 试题9&#xff1a; 试题1…

python游戏开发pygame初步

文章目录 安装和示例移动物体优化 安装和示例 顾名思义&#xff0c;PyGame就是用来做游戏的Python库&#xff0c;提供了许多游戏开发功能&#xff0c;如图像处理、音频播放、事件处理、碰撞检测等等。从这个角度来说&#xff0c;pygame不仅是一个游戏库&#xff0c;同时也是一…

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

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

前端web开发学习笔记

JavaWeb 前端Web开发HTMLCSSjavaScript1.JS引入2.JS基础语法3.JS函数4.JS对象 BOMDOM文档对象模型JS事件监听VueVue常用指令Vue的生命周期 AjaxAxios 前端工程化环境准备NodeJS安装和Vue-cli安装vue项目Vue组件库Element组件的使用 Vue路由Nginx打包部署 前端Web开发 HTML 负…

Python监控服务进程及自启动服务方法与实践

1. 需求概述 当我们在Windows Server环境中部署XX系统的实际应用中&#xff0c;往往会遇到一些运维管理的挑战。为了确保系统的持续稳定运行&#xff0c;特别是在服务程序因各种原因突然关闭的情况下&#xff0c;我们可以借助Python的强大生态系统来构建一个监控与自动重启的管…

BUUCTF [SWPU2019]伟大的侦探 1

BUUCTF:https://buuoj.cn/challenges 题目描述&#xff1a; 下载附件&#xff0c;解压提示需要密码&#xff0c;但解压出一个密码.txt文件。 密文&#xff1a; 解题思路&#xff1a; 1、打开密码.txt文件&#xff0c;提示如下。 压缩包密码:摂m墷m卪倕ⅲm仈Z 呜呜呜…

MYSQL基础知识之【添加数据,查询数据】

文章目录 前言MySQL 插入数据通过命令提示窗口插入数据使用PHP脚本插入数据 MySQL 查询数据通过命令提示符获取数据使用PHP脚本来获取数据内存释放 后言 前言 hello world欢迎来到前端的新世界 &#x1f61c;当前文章系列专栏&#xff1a;Mysql &#x1f431;‍&#x1f453;博…

C语言指针——从底层原理到应用

参考&#xff1a;C语言指针-从底层原理到花式技巧&#xff0c;用图文和代码帮你讲解透彻 目录 一、前言二、变量与指针的本质1. 内存地址2. 32位与64位系统3. 变量4. 指针变量5. 操作指针变量5.1 指针变量自身的值5.2 获取指针变量所指向的数据5.3 以什么样的数据类型来使用/解…

shiro的前后端分离模式

shiro的前后端分离模式 前言&#xff1a;在上一篇《shiro的简单认证和授权》中介绍了shiro的搭建&#xff0c;默认情况下&#xff0c;shiro是通过设置cookie&#xff0c;使前端请求带有“JSESSION”cookie&#xff0c;后端通过获取该cookie判断用户是否登录以及授权。但是在前…

什么是零拷贝 、零拷贝优化方案 - 真正的零拷贝,哪些地方会用到零拷贝技术

文章目录 什么是零拷贝3、零拷贝优化方案 - 真正的零拷贝哪些地方会用到零拷贝技术 现在来谈谈零拷贝&#xff0c;以及在开发中哪些地方使用到零拷贝。 开干… 什么是零拷贝 零拷贝指的是&#xff0c;从一个存储区域到另一个存储区域的copy任务无需CPU参与就可完成。零拷贝的底…

11-25碎片小知识

一.strlen补充 strlen函数返回值是size_t&#xff0c;即无符号整型&#xff0c; size_t有头文件&#xff0c;是stdio.h 由于strlen函数返回值是无符号整型&#xff0c;所以下面代码要注意 -3会被转换成无符号的 实现my_strlen 法一&#xff1a;指针减指针 #define _CRT_S…

CleanMyMac X好不好用?有哪些优势

CleanMyMac X2024正是这一愿景和使命的体现。 作为一个团队&#xff0c;我们致力于采用令人过目不忘的设计来打造我们引以为豪的产品。 这是 UX/UI 设计已经成为我们核心价值的原因之一。 这也是我们不断完善它&#xff0c;从而为我们的用户创造最神奇体验的动力。 CleanMyMac …