用组合模式实现新功能标新

255 阅读4分钟

需求背景

针对新功能要有比较醒目的展示方式,很多产品都会用小红点,蒙层引导,红色标签等方式来对UI元素进行标新处理

普通的标新逻辑

  • 用户点击标新的UI元素,则会将标新消除,退出应用再进来不会再展示标新。
  • 这些标新只在新的一个版本出现,用户卸载重装该新版本可重复出现标新。

复杂标新的处理

  • 相同的标新有多处UI元素
  • 标新的处理具有依赖关系。比如QQ会话里输入框的表情图标,只要在表情面板底部里存在标新则会展示小红点,只有里面所有标新都消除,输入框的表情图标才不会展示小红点

发现问题

对于普通的标新处理,最简单的方式便是通过SharedPreference(后续简称Sp)存储特定UI元素标新的Boolean变量,默认为true,显示标新,点击则设置为false,不显示标新

public class MySpUtil {
    private static final String KEY_SHOW_CONV_LIST_TITLE_RIGHT_RED_POINT ="right.red.point";
  
    public boolean isShowConvListRedPoint() {
        return mPreferences.getBoolean(KEY_SHOW_CONV_LIST_TITLE_RIGHT_RED_POINT, true);
    }
    public void setShowConvListRedPoint(boolean isShow) {
        mPreferences.edit().putBoolean(KEY_SHOW_CONV_LIST_TITLE_RIGHT_RED_POINT,isShow).apply();
    }
}

随着业务场景不断扩展,新功能越来越多,标新元素的页不断增多,如果简单地在Sp工具类里记录要标新元素的key和提供对应的get和set方法,则Sp工具类会变得臃肿,扩展性不佳

Sp作为一个存储的通道,封装Sp的工具类对外方法不应该耦合太多的业务属性,且在对于要处理依赖关系的标新场景,上述处理方法对各种标新元素的维护就会变得复杂

解决思路

基于面向对象编程的思想,我们将标新的事物作为对象,来分析其状态和行为。从上述需求描述可知,该对象有以下特性:

  • 状态:未读(标新)/已读(消除标新)
  • 行为:可以被设置为已读

在存在依赖关系的需求场景下,这些对象可以被描述成整体-部分的关系组织到树型结构中,因此可以采用组合模式实现系统内各个对象的行为操作一致性

talk is cheap,具体的方案可以分三步走:

设置标新功能统一存储规则和方法

在底层存储上,我们还是采用SharedPreference,与之前不同的是,我们需要制定适用于以后所有标新场景通用的存储规则。

  • 先定义一个用于标新功能的key前缀KEY_NEW_ITEM,然后通过方法参数让外部传入itemKey,将两者拼接形成对一个特定标新对象的key
  • 提供判断是否标新和设置标新消除方法
public class MySpUtil {
  private static final String KEY_NEW_ITEM = "key.new.item";
  private SharedPreferences mPreferences;
  
  public static MySpUtil getInstance() {
    return MySpUtil.InstanceHolder.sInstance;
  }
 
  private static class InstanceHolder {
    static MySpUtil sInstance = new MySpUtil();
  }
 
  private MySpUtil() {
    mPreferences = ContextHolder.appContext()
    .getSharedPreferences("mySpUtil", Context.MODE_PRIVATE);
  }
 
  public boolean isNewItemRead(String itemKey) {
    return mPreferences.getBoolean(KEY_NEW_ITEM + itemKey, false);
  }
 
  public void setNewItemHasRead(String itemKey) {
    mPreferences.edit().putBoolean(KEY_NEW_ITEM + itemKey, true).apply();
  }

采用组合模式构建标新系统

在Android很多场景都用到了组合模式,比如View和ViewGroup便是很典型的例子。在本次分析的标新功能里,用组合模式实现可以带给我们很多好处,我们将要标新的事物用具体的类来描述,而不是像之前耦合在Sp的工具类的XXXKey字段中。

  public interface INewItem {
    boolean isRead();
    void setHasRead();
  }
 
  public class NewItem implements INewItem {
    private String itemKey;
 
    public NewItem(String itemKey) {
      this.itemKey = itemKey;
    }
 
    @Override
    public boolean isRead() {
      return MySpUtil.getInstance().isNewItemRead(itemKey);
    }
 
    @Override
    public void setHasRead() {
      MySpUtil.getInstance().setNewItemHasRead(itemKey);
    }
  }
 
  public class NewItemGroup implements INewItem {
    List<INewItem> children = new ArrayList<>();
 
    @Override
    public boolean isRead() {
      for (INewItem child : children) {
        if (!child.isRead()) {
          return false;
        }
      }
      return true;
    }
 
    @Override
    public void setHasRead() {
      for (INewItem child : children) {
        child.setHasRead();
      }
    }
 
    public void addChild(INewItem child) {
      children.add(child);
    }
  }

使用姿势

以上面QQ的表情图标标新案例为例,我们用DIYNewItem类表示表情面板下方的DIY创造自己的表情功能的小红点标新,用EmoticonNewItem类表示输入框表情图标的小红点标新。

public class DIYNewItem extends NewItem {
  private static final String KEY = "DIYNewItem";
  public DIYNewItem() {
    super(KEY);
  }
}
 
public class EmoticonNewItem extends NewItemGroup {
  public EmoticonNewItem() {
    addChild(new DIYNewItem());
  }
}

// 构建标新事物对象
DIYNewItem diyNewItem = new DIYNewItem();
EmoticonNewItem emoticonNewItem = new EmoticonNewItem();

// 根据标新事物的状态设置小红点是否显示
diyRedPointView.setVisibility(diyNewItem.isRead() ? View.GONE : View.VISIBLE);
emoticonRedPointView.setVisibility(emoticonNewItem.isRead() ?  View.GONE : View.VISIBLE);

// 点击diy图标,更新DIY标新对象状态,刷新界面的小红点显示
diyIcon.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
      diyNewItem.setHasRead();
      diyRedPointView.setVisibility(diyNewItem.isRead() ? View.GONE : View.VISIBLE);
      emoticonRedPointView.setVisibility(emoticonNewItem.isRead() ?  View.GONE : View.VISIBLE);
    }
  });

项目地址

github.com/bingocode/B…