代码地址:https://github.com/Nagi1225/NeoPreference.git
最初在开发NeoPreference这个SharedPreferences工具的时候,就期望完成三个目标:
- 代码简洁,新增配置项的时候一行代码(最多两行);
- 读写安全,包括数据类型安全,支持类型的进一步修饰,例如,可以指定整数范围;
- 可以自动生成配置页,新增配置项的时候不需要手动去页面上添加。
前两个目标已经完成,参见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) {}};}
}
页面显示效果
- 根据配置项自动生成的页面:
- 配置项对应的编辑弹窗:
)