代码地址: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
@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) {
}
};
}
}
页面显示效果
- 根据配置项自动生成的页面:
- 配置项对应的编辑弹窗:
)