在常规的业务开发中,我们往往是先构建界面元素,然后在Activity中赋能界面元素相关业务能力,譬如下图所示的编辑采购订单场景:
由此可以看到,我们将大量的业务代码充斥在Activity中,而且仅实现了界面控件级别的复用,业务级别的代码基本都是各种复制,由此也催生出一种想法:
我们能不能做到业务控件级别的复用?
这就是本篇《业务套件》的由来,我们期望可以达成:
为了方便理解,我们在最开始先针对上述提及的4个核心过程,分别提供
其核心承载功能就是,根据 业务控件类型,生成其真实的视图控件
public interface UiViewPool {
/**
* 根据 业务控件类型, 返回对应的 真实视图控件
* @param uiType 业务控件类型 {@link com.sangfor.pocket.uin.newway.UiItemTypes.ViewType}
* @param parant 当前控件将被加入的父布局
* @return 真实的视图控件
*/
View getView(int uiType, ViewGroup parant);
}
具体已经支持的业务控件类型和视图控件类型,可以查看com.sangfor.pocket.uin.newway.UiItemTypes
public class UiItemTypes {
public static class ViewType {
public static final int VT_UNKNOWN = 0;
public static final int VT_LinearLayout_Vertical = 10;
...
}
public static class UiType {
public static final int UT_SELECT_NORMAL = 1;
public static final int UT_SINGLE_SELECT_CUSTM = 4;
public static final int UT_MULTIPLE_SELECT_CUSTM = 5;
...
}
}
由于整体框架遵循”业务控件“高于”视图控件“的一致性规则,因此,即便是一些不具备业务属性的基础视图控件,我们也不能直接使用其视图控件。而是需要定义其对应的业务控件去使用
其实就是一个 对应的转换关系,将 UiType业务控件类型 转换为 viewType视图控件类型
public class UiTypeMapToViewType {
public int mapWithType(int uiType){
int type;
switch (uiType){
case UiItemTypes.UiType.UT_SELECT_NORMAL:
case UiItemTypes.UiType.UT_SELECT_TIME:
case UiItemTypes.UiType.UT_SINGLE_SELECT_CUSTM:
case UiItemTypes.UiType.UT_MULTIPLE_SELECT_CUSTM:
case UiItemTypes.UiType.UT_SINGLE_PERSON:
case UiItemTypes.UiType.UT_SINGLE_SELECT_CUSTM_BY_SALE_ORDER:
type = UiItemTypes.ViewType.VT_TextImageNormalForm;
break;
case UiItemTypes.UiType.UT_CLASS_TITLE:
//左边显示是表单
type = UiItemTypes.ViewType.VT_CLASS_TITLE;
break;
//...
}
}
}
上文我们已经说了,UiViewPool的主要目的就是
生成视图控件View并返回是UiViewPool的核心功能
它的核心实现就是借助 ViewMaker来实现的:
Map pool = new HashMap<>();
视图控件类型ViewType 与对应 ViewMaker视图控件制造器 的MapUiTypeMapToViewType
业务控件UiType和视图控件ViewType转换器,对UiType进行转换为viewtypepublic abstract class BaseUiViewPool implements UiViewPool {
protected Context context;
/**
* 视图控件类型ViewType 与对应 ViewMaker视图控件制造器 的Map
*/
protected Map pool = new HashMap<>();
public BaseUiViewPool(Context context) {
//上下文对象,用于创建视图控件
this.context = context;
//业务控件UiType 和 视图控件ViewType 转换器
map = new UiTypeMapToViewType();
//构建支持的 视图制造器列表
ViewMaker[] viewMakers = listViewMakers();
if(viewMakers != null && viewMakers.length > 0){
for(ViewMaker vm : viewMakers){
ViewMaker put = pool.put(vm.getViewType(), vm);
}
}
}
@Override
public View getView(int uiType, ViewGroup parent) {
ViewMaker viewMaker;
if(uiType == UiItemTypes.ViewType.VT_UNKNOWN){
viewMaker = UNKNOWN_VIEW_MAKER;
}else {
//1. 借助`UiTypeMapToViewType`业务控件UiType和视图控件ViewType转换器,对UiType进行转换为viewtype
//2. 获取viewtype对应的ViewMaker
viewMaker = pool.get(map.mapWithType(uiType));
if (viewMaker == null) {
if (BuildConfig.DEBUG) {
throw new IllegalStateException("Unsupported uiType [" + uiType + "]");
}
viewMaker = UNSUPPORTED_VIEW_MAKER;
}
}
//3. 使用viewtype对应的ViewMaker,new出真正的视图控件
return viewMaker.makeView(context, parent);
}
}
在上个环节我们讲到了,借助Map
视图控件类型ViewType 与对应 ViewMaker视图控件制造器 的Map,来获取对应的 ViewMaker
那么这个 HashMap 是怎么初始化的呢?
public abstract class BaseUiViewPool implements UiViewPool {
protected abstract ViewMaker[] listViewMakers();
}
具体的,我们现在有三个子类,我们分别看下他们的实现:
public class StandardViewPool extends BaseUiViewPool {
public StandardViewPool(Context context) {
super(context);
}
@Override
protected ViewMaker[] listViewMakers() {
return new ViewMaker[]{
new FlexiblePictureLayoutMaker(),
new ApprovalShowViewMaker(),
new RemovableOrangeClassifyTitleMaker(),
new AddButtonViewMaker(),
new InfoStatusBarViewMaker(),
new EmptyViewMaker()
};
}
}
public class FormViewPool extends BaseUiViewPool {
public FormViewPool(Context context) {
super(context);
}
@Override
protected ViewMaker[] listViewMakers() {
return new ViewMaker[]{
new TextEditableFormMaker(),
new TextImageNormalFormMaker(),
new LeftWrapContentTextImageNormalFormMaker(),
new FormTipsMaker(),
new IsolatedFormButtonMaker(),
new LeftRightTitleMaker(),
new PureEditableFormMaker()
};
}
}
public class JxcViewPool extends BaseUiViewPool {
public JxcViewPool(Context context) {
super(context);
}
@Override
protected ViewMaker[] listViewMakers() {
return new ViewMaker[]{
new StockCheckDetailsViewMaker(),
new StockCheckProductCreateViewMaker(),
new StockCheckProductShowViewMaker(),
new JxcProductItemViewMaker(),
};
}
}
需要注意的是,我们在上文列举的StandardViewPool
,FormViewPool
,JxcViewPool
都是直接继承BaseUiViewPool
这也就意味着他们支持的功能区域是各自独立的,那么我如何同时使用不同Pool呢?
/**
* 视图控件池的组合能力
* @param other
* @return
*/
public BaseUiViewPool merge(BaseUiViewPool other) {
pool.putAll(other.pool);
return this;
}
我们不使用继承结构,也是为了遵循开发中的”组合高于继承“的原则
根据当前指定的int 控件类型,生成对应的实际控件。
这是对**new 视图控件
**能力的一种抽象,借助该制造器赋予框架生成对应试图控件的能力
public interface ViewMaker {
/**
* 当前 制造器 对应的 viewtype视图控件类型
* 该值不能随意定义,必须集中管理为 {@link com.sangfor.pocket.uin.newway.UiItemTypes.ViewType}中自定义
* @return
*/
int getViewType();
/**
* 当前制造器 对应的 视图控件
* @param context new控件时需要的上下文
* @param parent 控件将被包含的parent group
* @return
*/
View makeView(Context context, ViewGroup parent);
}
目前支持的视图控件,主要包含以下样式(com.sangfor.pocket.uin.newway.UiItemTypes#ViewType
):
//框架自定义
public static final int VT_UNKNOWN = 0;
public static final int VT_STANDARD_GROUP = 6;
//android 原生View
public static final int VT_LinearLayout_Vertical = 10;
public static final int VT_EMPYT = 19;
//moa标准表单项
public static final int VT_TextEditableForm = 1;
public static final int VT_TextImageNormalForm = 2;
public static final int VT_FlexiblePictureLayout = 3;
public static final int VT_LEFT_WRAP_TEXT_IMAGE_NORMAL_FORM = 5;
public static final int VT_CLASS_TITLE = 7;
public static final int VT_ADD_BUTTON_VIEW = 8;//底部的 添加button
public static final int VT_PRODUCT_ITEM_VIEW = 9;//jxc的一个商品item,包含商品名称、批次号、数量、总价等
public static final int VT_FormTips = 11;
public static final int VT_IsolatedFormButton = 12;
public static final int VT_StockCheckDetailsView = 13;
public static final int VT_LeftRightTitle = 14;
public static final int VT_StockCheckProductCreateView = 15;
public static final int VT_InfoStatusBar = 16;
public static final int VT_StockCheckProductShowView = 17;
public static final int VT_PURE_EDITABLEFORM = 18;
//审批人
public static final int VT_APPROVAL_SHOW_VIEW = 4;
更直观的,我们简单的看两个实例:
public class TextImageNormalFormMaker implements ViewMaker {
@Override
public int getViewType() {
return UiItemTypes.ViewType.VT_TextImageNormalForm;
}
@Override
public View makeView(Context context, ViewGroup parent) {
return new TextImageNormalForm(context);
}
}
public class VerticalLinearLayoutMaker implements ViewMaker {
@Override
public int getViewType() {
return UiItemTypes.ViewType.VT_LinearLayout_Vertical;
}
@Override
public View makeView(Context context, ViewGroup parent) {
LinearLayout ll = new LinearLayout(context);
ll.setOrientation(LinearLayout.VERTICAL);
return ll;
}
}
UIItem是对业务控件的一种抽象
在了解 UiItem之前,我们必须清晰的意识到,业务控件其本质还是一种视图控件。因此,视图控件所具备的一些能力,业务控件也会被具有和抽象出来
我们思考一个场景:选择客户,并据此进行分析
针对这是一个视图控件
针对它具备一定的业务能力,也就是说和 CustmerVo相关
UiItem的不保留活动的支持
public interface UiValue extends Parcelable, Cloneable{
/**
* 获取当前UiValue对应的界面渲染值
*/
String getStringValue();
/**
* 获取当前UiValue对应的VO实例对象
* 将会被{@link UiItem#extractFromGetter()} 赋值为 Getter中的引用
* 或者被更新操作赋值为一个新的引用
*
* 一般来说 UiValue的实现类中会持有一个 vo对象的引用:
* 1. **在初始化Getter过程中,该引用指向 业务代码中的原对象;**
* 2. **在更新后(譬如选择了一个新的供应商分类),该引用会指向一个新的对象**
*/
V getValue();
/**
* 判断UiValue是否差异,用于在 {@link UiItem#isChanged()}
*/
boolean isTheSame(UiValue other);
/**
* 用于UiValue的备份,用于在 {@link UiItem#backup()}
* 表示为界面初始值,将最终支持 {@link UiItem#isChanged()},以判断是否发生更新
*/
UiValue copy();
}
我们来看一个供应商分类选择的实例:
public class JxcSelectSupplierclassUiValue extends BaseUiValue implements Parcelable {
private SupplierClass supplierClass;
public JxcSelectSupplierclassUiValue(SupplierClass sClass) {
this.supplierClass = sClass;
}
@Override
public String getStringValue() {
if (supplierClass != null && !TextUtils.isEmpty(supplierClass.classname)) {
return supplierClass.classname;
}
return "";
}
@Override
public SupplierClass getValue() {
return supplierClass;
}
@Override
public boolean isTheSame(UiValue other) {
if (other instanceof JxcSelectSupplierclassUiValue) {
SupplierClass otherSupplierClass = ((JxcSelectSupplierclassUiValue) other).supplierClass;
return supplierClass.classId == otherSupplierClass.classId && supplierClass.classname == otherSupplierClass.classname;
}
return false;
}
}
尤其需要注意一点,一般来说 UiValue的实现类中会持有一个 vo对象的引用:
Getter|Setter是作用在类级别的,如果想针对某个VO操作,必须是传入某个Vo对象实例的内部成员变量,从而实现get|setter的响应都会针对当前Vo对象实例
public interface Getter {
T get();
}
public interface Setter {
void set(T data);
}
我们来看个例子:
public static class MyData {
public CustomerLineVo custm;
private Setter custmSetter = new Setter() {
@Override
public void set(CustomerLineVo data) {
custm = data;
}
};
private Getter custmGetter = new Getter() {
@Override
public CustomerLineVo get() {
return custm;
}
};
}
MyData myData = new MyData();
SingleSelectCustmUiItem selectCustmUiItem = new SingleSelectCustmUiItem(this);
selectCustmUiItem.setSetter(myData.custmSetter());
selectCustmUiItem.setGetter(myData.custmGetter);
在BaseUiItem中,我们可以观察到这种串联关系
从某种层次上,Getter、Setter的操作是高于 UIValue的。
这是由于,如果说模型”用户–>VO–>UIValue—>界面渲染“中UIValue割据了用户对View的直接操作,Getter|Setter相当于 框架和Vo打交道,而UiValue则是框架如何根据VO进行界面渲染和反向返回
我们先来看 ”VO->UIValue–>界面渲染“的过程,extractFromGetter()
:
@Override
public void extractFromGetter() {
if(getter != null) {
Object value = getter.get();
if (getConvertor != null) {
value = getConvertor.convert(value);
}
UiValue uiValue = getUiValueGenerator().generateUiValue(value);
setValue(uiValue);
}
}
@Override
public void setValue(UiValue value){
this.uiValue = value;
VT vt = mutexHolder.get();
if(vt != null){
updateValue(vt, uiValue);
}
}
可以看出基本遵循以下路线:
extractFromGetter()
作为入口Getter
,获取到VO对象实例UIValue构造器UiValueGenerator
构建UiValuesetValue(UiValue value)
在BaseUiItem基本业务控件
提供了以下和UiValueGenerator相关的函数,基本可以看出就是以下默认的get|set函数
@Override
public void setUiValueGenerator(UiValueGenerator uiValueGenerator) {
this.uiValueGenerator = uiValueGenerator;
}
/**
* 提供一个 默认的 UiValue生成器
* @return
*/
protected UiValueGenerator provideDefaultUiValueGenerator(){
return null;
}
protected UiValueGenerator getUiValueGenerator(){
if(uiValueGenerator == null){
uiValueGenerator = provideDefaultUiValueGenerator();
}
return uiValueGenerator;
}
那么 UiValueGenerator 到底是什么呢?
它实际上承载着 ”VO->UIValue–>界面渲染“的过程中,”Vo–>UiValue”的过程“
不同的自定UiItem通过 重写provideDefaultUiValueGenerator()
实现 当前Item控制自己的转换
UIValue被更新后,getValue()所指向的引用将不再是原来Getter的对象地址,而是一个新的对象
譬如选了一个新的客户被赋值到 UIValue的一个自定义的引用里,因此需要我们调用setter去赋值
@Override
public UiValue getValue() {
return uiValue;
}
@Override
public void fillInSetter() {
if(setter != null) {
UiValue uiValue = getValue();
Object value = null;
if(uiValue != null) {
value = uiValue.getValue();
}
if (setConvertor != null) {
value = setConvertor.convert(value);
}
setter.set(value);
}
}
可以看出基本遵循以下路线:
fillInSetter()
作为入口getValue()
,获取到 界面渲染值。Setter
的set函数给目标对象赋值public class JxcSelectSupplierClassUiItem extends TextImageNormalFormUiItem implements UiItemResultGetter {
public JxcSelectSupplierclassUiValue mUiValue;
public JxcSelectSupplierClassUiItem(Context context) {
super(context);
}
@Override
public void onClick() {
JxcSelectSupplierclassUiValue supplierClass = null;
if (getValue() != null && getValue() instanceof JxcSelectSupplierclassUiValue){
supplierClass = (JxcSelectSupplierclassUiValue)getValue();
}
SupplierIntentManager.intentToGetSupplierClass(getActivityStarter(), getRequestCode(), getRequestTag(0), supplierClass == null ? null : supplierClass.getValue());
}
@Override
public void getResult(Intent data, int operationTag) {
SupplierClass supplierClass = data.getParcelableExtra(SupplierIntentManager.KEY_JXC_SECECT_SUPPLIER_CLASS);
if (supplierClass != null) {
mUiValue = new JxcSelectSupplierclassUiValue(supplierClass.classId == -1 ? null : supplierClass);
setValue(mUiValue);
}
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeParcelable(this.mUiValue, flags);
}
protected JxcSelectSupplierClassUiItem(Parcel in) {
super(in);
this.mUiValue = in.readParcelable(JxcSelectSupplierclassUiValue.class.getClassLoader());
}
public static final Creator CREATOR = new Creator() {
@Override
public JxcSelectSupplierClassUiItem createFromParcel(Parcel source) {
return new JxcSelectSupplierClassUiItem(source);
}
@Override
public JxcSelectSupplierClassUiItem[] newArray(int size) {
return new JxcSelectSupplierClassUiItem[size];
}
};
}
UiInteraction是暴露给用户的操作手柄
从某种意义上,UiInteraction类似于RecyleView,回想下我们使用RecyleView时,仅需要配置datas、bindView和bindData就可以实现界面渲染
Item的Ui视图控件生成通过调用
UiInteraction#commit
或者UiInteraction#notifySetChanged()
触发,当然内部会对视图控件进行 属性和渲染值的初始化设定
Item的Ui属性由ITem自己控制,调用其UiItem#commitUiProps()
的函数触发其私有函数updateUiProps(VT view)
Item的UI渲染值通过UiInteraction#dumpOnItem()
—>item.extractFromGetter
—>item.setValue
—>item.updateValue
public interface UiInteraction extends ActivityResultReceiver, SavableForm, ContextProvider, TimeConsumingOperationStatistic {
int MODE_INDEX_DEPENDING_ON_SRC = ListPendingOperationLogic.MODE_INDEX_DEPENDING_ON_SRC;
int MODE_INDEX_DYNAMIC = ListPendingOperationLogic.MODE_INDEX_DYNAMIC;
/***********************************针对List items***********************************/
/**
* 增
*/
//设置一个set操作, 需要调用commit来提交
UiInteraction setItems(List items);
//设置一个insert操作, 需要调用commit来提交
UiInteraction insertItem(int index, UiItem item);
//设置一个append操作, 需要调用commit来提交
UiInteraction appendItems(List items);
/**
* 删
*/
//设置一个remove操作, 需要调用commit来提交
UiInteraction removeItem(UiItem item);
//设置一个remove操作, 需要调用commit来提交
UiInteraction removeItem(int index);
/**
* 查:单个
*/
//通过一个用户定义的id 来搜寻 一个item,该item处在pending状态, 即commit方法未被调用, 会遍历树状结构,直到找到一个符合的为止
UiItem findItemInPendingStateByUserId(int userId);
//通过一个用户定义的id 来搜寻 一个item,会遍历树状结构,直到找到一个符合的为止
UiItem findItemByUserId(int userId);
//返回未经扩展的index位置的item
UiItem getItem(int index);
/**
* 查:全部
*/
//返回未经扩展的item 列表
List getItems();
//返回经过扩展的item列表(UiItemGroup被扩展成了子item )
List getExtendedItems();
/**
* 由于在树形结构中查找一个Uitem是要进行递归调用的,因此
* 1. 用户可以指定先把一些UiItem加入到缓存池中,以加快查询过程
* 2. 框架在查找UiItem时,也会将其主动添加到缓存池中
*/
//缓存一个item
void cacheItem(int userId, UiItem item);
//从缓存里获取item
UiItem getItemFromCache(int userId);
//先从缓存里查找,没有则从item树里找,找到更新到缓存
UiItem getAnItem(int userId);
/*****************************针对bindView的操作 也就是构建我们常规意义上的xml中各个view的过程************************/
/**
* 提交一系列操作
*/
void commit();
/**
* 退出操作
*/
void abort();
/**
* 全量更新UI:会导致全部初始化,类似与 setItems(newDatas).commit();
*/
void notifyItemSetChanged();
/***************针对bindData的基础布局的vo渲染,也就是我们常规意义上的针对xml的一些View配置成我们vo的某些界面显示*************/
/**
* 用item中的值填充实体, 需要先调用 item.setSetter 方法来进行绑定
* ps: 界面值 和 界面目标对象的属性 并不是实时对应关系,调用此方法,赋值到目标对象实例中的指定属性上
*/
void fillEntityUp();
/**
* 用实体中的字段值去填充界面item, 需要先调用 item.setGetter 方法来进行绑定
*/
void dumpOnItem();
void dumpOnItem(int userId);
/***********************************************针对Result响应的********************************************/
void setRequestCode(int requestCode);
void setActivityStarter(ActivityStarter activityStarter);
void setOnUiItemResultListener(OnUiItemResultListener listener);
/******************************************判断界面内容是否改变******************************************/
/**
* 备份界面内容
*/
void backup();
/**
* 备份userId对应的item
* @param userId
*/
void backup(int userId);
/**
* 判断界面内容是否改变
* @return
*/
boolean isChanged();
/******************************************针对各个uiItem的vo合法性的检测******************************************/
@Deprecated
CheckResult check();
CheckResult2 check2();
List checkAll();
/*************************************配置项和辅助utils****************************************/
/**
* 回填数据时,是否仅返回变动了的item
* @param onlyGetChangedValue
*/
void setOnlyGetChangedValue(boolean onlyGetChangedValue);
/**
* toast
* @param prompt
*/
void setPrompt(Prompt prompt);
// UiItem moveTo(int index);
/**
* 设置一个监听UiItem value值变化的监听器
* @param watcher
*/
void setUiItemValueWatcher(UiItemValueWatcher watcher);
void setOnUiItemClickListener(UiItem.OnUiItemClickListener listener);
/**
* 设置一系列未执行(commit) 的操作的下标的意义 默认为 MODE_INDEX_DEPENDING_ON_SRC
* @param mode
*/
void setIndexMode(int mode);
/*************************************针对不保留活动的**************************************/
UiInteraction autoRestore();
/**
* 当界面被销毁,重新进入,items被恢复 时调用此方法, 需要调用commit来提交
* @param items
* @return
*/
UiInteraction restoreItems(List items);
boolean isThereAnyPendingOperation();
void setTimeConsumingOperationWatcher(TimeConsumingOperationWatcher timeConsumingOperationWatcher);
复用级别在UiItem,也就是说需要掌握UiItem及其相关类的创建方法