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. 根据配置项自动生成的页面:

NeoPreference延伸:为SharedPreferences配置项生成配置页面_第1张图片

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

NeoPreference延伸:为SharedPreferences配置项生成配置页面_第2张图片)

你可能感兴趣的:(Java,java,开发语言,android,sharedprefere,preference)