搭建自己的数据绑定框架

5/1/2022 框架

# 一、简介

我们在搭建MVVM框架时一般都需要配合databinding一起使用,只需要在layout.xml中使用layout标签+data标签,编译器就可以为我们自动生成相应的viewBinding类,当为这些view Binding类设置viewModel后,就可以在viewModel中通知view进行更新了。

MVVM的核心思想是数据驱动,这篇文章就是借鉴了databinding的部分实现方式搭建的一套简易的数据绑定框架。

# 二、整体架构

整体架构.jpg

# 三、搭建步骤

先定义view层与viewModel层之间的接口

/**
 * 可被观察的包含binding属性的数据模型,viewModel进行实现
 */
public interface PropertiesObservable {
    void addOnPropertyChangedCallback(OnPropertyChangedCallback callback);

    void removeOnPropertyChangedCallback(OnPropertyChangedCallback callback);

    /**
     * 实际的观察者,viewBinding进行实现
     */
    abstract class OnPropertyChangedCallback {
        public abstract void onPropertyChanged(PropertiesObservable sender, Enum<?> propertyId);
    }
}

接着在viewModel层将来自view层的观察者用弱引用包装后进行存储

public class BaseViewModel extends AndroidViewModel implements PropertiesObservable {
    //观察数据变化的所有对象,用弱引用进行包装防止内存泄漏
    private final Set<WeakReference<OnPropertyChangedCallback>> propertyChangedCallbacks = new HashSet<>();

    @Override
    public void addOnPropertyChangedCallback(OnPropertyChangedCallback callback) {
        propertyChangedCallbacks.add(new WeakReference<>(callback));
        ...
    }

    @Override
    public void removeOnPropertyChangedCallback(OnPropertyChangedCallback callback) {
        for (WeakReference<OnPropertyChangedCallback> weakReference : propertyChangedCallbacks) {
            OnPropertyChangedCallback changedCallback = weakReference.get();
            if (callback == changedCallback) {
                propertyChangedCallbacks.remove(weakReference);
                break;
            }
        }

        ...
    }

在view层viewBinding侧进行注册监听

/**
 * 设置viewModel并注册监听viewModel的数据变化
 */
protected void setViewModel(BaseViewModel viewModel) {
    if (this.viewModel != null) {
        this.viewModel.removeOnPropertyChangedCallback(this);
    }
    this.viewModel = viewModel;
    this.viewModel.addOnPropertyChangedCallback(this);
    ...
}

此时我们已经利用了观察者模式实现了view层对viewModel层的监听,但还需要明确的知道viewModel层的谁数据变化了,以及view层的谁绑定了该数据,显然需要一个key在viewModel层关联方法,在view层关联View,而需要绑定的数据在源码阶段就已经确定了,所以用常量和枚举都可以,这里选择枚举,另外需要绑定的数据这里也简单一点指定到某个方法。

由于方法名并不是固定的,在view层利用反射调用虽然可行,但用代理类进行包装显然更加合理。

public interface OnBindingCallback<T> {
    T get();
}

这样在view层只要能拿到枚举key对应的OnBindingCallback对象并调用其get方法就能拿到需要绑定的数据方法的最新值了。

再定义一个类用于存储枚举key和其对应的代理对象

public class BindingCallbacks {
    protected Map<Enum<?>, OnBindingCallback<?>> callbacks = new HashMap<>();

    /**
     * 获取实际方法的OnBindingCallback代理对象
     */
    public OnBindingCallback<?> getCallback(Enum<?> type) {
        return callbacks.get(type);
    }
}

手动添加枚举key和其对应的代理对象显然不够优雅,我们借鉴databinding引入apt技术,定义一个注解用来标记需要绑定的数据方法

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE) //源码级别保留,编译后丢弃
public @interface Binding {
}

并在编译阶段生成相应的枚举key后自动将其加入到BindingCallbacks.callbacks中

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    ...

    for (Element element : roundEnvironment.getElementsAnnotatedWith(Binding.class)) {
        if (element.getKind() == ElementKind.METHOD && element.getModifiers().contains(Modifier.PUBLIC)) {
            ExecutableElement executableElement = (ExecutableElement) element;
            if (executableElement.getParameters().size() == 0) {
                ...
                //当方法名为get和is开头时移除该关键字
                String functionName = element.getSimpleName().toString();
                if (functionName.startsWith("get")) {
                    functionName = functionName.substring(3);
                } else if (functionName.startsWith("is")) {
                    functionName = functionName.substring(2);
                }

                //将其后的第一个字母设为小写
                functionName = functionName.substring(0, 1).toLowerCase() + functionName.substring(1);

                //将该处理过的方法名作为枚举的一个类别
                if (!enumConstants.contains(functionName)) {
                    enumSpecBuilder.addEnumConstant(functionName);
                    enumConstants.add(functionName);
                }
                //为BindingCallbacks的构造器添加一对枚举类别和该方法的对应关系
                classHolder.constructorSpecBuilder.addStatement("callbacks.put($T.$L, new $T(){public $T get(){return model.$L();}})", ClassName.bestGuess(masterPkg + "." + CLASS_NAME_ENUM), functionName, ClassName.bestGuess(CLASS_CALLBACK_LISTENER), TypeName.get(executableElement.getReturnType()).box(), executableElement.getSimpleName().toString());
            }
        }
    }

    if (masterPkg != null) {
        for (String key : bindingMap.keySet()) {
            ClassHolder classHolder = bindingMap.get(key);
            classHolder.classSpecBuilder.addMethod(classHolder.constructorSpecBuilder.build());
            //生成BindingCallbacks特定类
            writeClass(key.substring(0, key.lastIndexOf('.')), bindingMap.get(key).classSpecBuilder.build(), filer);
        }
        //生成BDR枚举类
        writeClass(masterPkg, enumSpecBuilder.build(), filer);
    }

    return true;
}

直接看下编译后的自动生成的类便于理解

 public class ListViewModel$BindingCallbacks extends BindingCallbacks {
   public ListViewModel$BindingCallbacks(ListViewModel model) {
     callbacks.put(BDR.emptyText, new OnBindingCallback(){
       public String get(){
         return model.getEmptyText();
       }
     });
     ...
   }
 }

Processor自动为每个ViewModel创建一个特定的BindingCallbacks,并在其构造器里将ViewModel中加了注解的方法进行包装,使用去除了get关键字的方法名作为枚举key,put进callbacks中,这样只要创建该特定的BindingCallbacks对象就能从callbacks中拿到所有的代理对象。

public BaseViewModel(@NonNull Application application) {
    super(application);

    bindingCallbacks = new BindingCallbacks();
    Class<?> thisClass = getClass();

    //查找当前对象到最底层BaseViewModel之间所有的枚举key和代理对象
    while (thisClass != null && thisClass != BaseViewModel.class) {
        BindingCallbacks callbacks = getBindingCallbacks(thisClass);
        if (callbacks != null) {
            bindingCallbacks.callbacks.putAll(callbacks.callbacks);
        }
        thisClass = thisClass.getSuperclass();
    }
}

/**
 * 通过反射创建viewModel对应BindingCallbacks类
 */
private BindingCallbacks getBindingCallbacks(Class<?> clazz) {
    String classNameWithPkg = clazz.getName() + "$BindingCallbacks";
    try {
        Class<?> callbacksClass = Class.forName(classNameWithPkg);
        return (BindingCallbacks) callbacksClass.getConstructor(clazz).newInstance(this);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

此时viewModel侧的基础封装都已经完成,通过viewModel->BindingCallbacks->key:OnBindingCallback->找到需要绑定的具体方法。

接下来搭建view层的基础框架

定义一个赋值接口类,用于给不同类型的view赋值

/**
 * 赋值接口类
 */
public interface ValueBinder<V extends View, Val> {
    void setValue(V view, Val value);
}

再定义一个BindHolder用于存储已绑定的view和对应的赋值器对象

private static class BindHolder {
    final View view;
    final ValueBinder valueBinder;

    BindHolder(View view, ValueBinder valueBinder) {
        this.view = view;
        this.valueBinder = valueBinder;
    }
}

通过bind方法将view、ValueBinder和枚举key三者进行关联

private final Map<Enum<?>, BindHolder> bindingView = new HashMap<>();


/**
 * 通过id进行绑定
 */
public void bind(int viewId, ValueBinder<? extends View, ?> valueBinder, Enum<?> type) {
    View view = rootView.findViewById(viewId);
    bind(view, valueBinder, type);
}

/**
 * 通过view进行绑定
 */
public void bind(View view, ValueBinder<? extends View, ?> valueBinder, Enum<?> type) {
    if (view != null) {
        putAndSet(new BindHolder(view, valueBinder), type);
    }
}

private void putAndSet(BindHolder bindHolder, Enum<?> type) {
    //dui对绑定关系进行保存
    bindingView.put(type, bindHolder);
    if (viewModel != null) {
        //获取被绑定方法的包装类
        OnBindingCallback<?> onBindingCallback = viewModel.getCallback(type);
        if (onBindingCallback != null) {
            //调用get方法就是调用的被绑定的方法,拿到返回值后交给valueBinder进行赋值
            bindHolder.valueBinder.setValue(bindHolder.view, onBindingCallback.get());
        }
    }
}

最后处理来自viewModel层的数据变化事件

@Override
public void onPropertyChanged(PropertiesObservable sender, Enum<?> propertyId) {
    //如果propertyId为BDR.all则更新所有已绑定的view
    if (propertyId == null || propertyId.ordinal() == 0) {
        resetAllValues();
        return;
    }

    if (bindingView.containsKey(propertyId)) {
        //根据枚举key找到绑定的view
        BindHolder bindHolder = bindingView.get(propertyId);
        if (bindHolder != null && bindHolder.view != null) {
            //根据枚举key找到绑定的代理类
            OnBindingCallback<?> onBindingCallback = viewModel.getCallback(propertyId);
            if (onBindingCallback != null) {
                //调用代理类的get方法获取最新的值后赋值给view
                //noinspection unchecked
                bindHolder.valueBinder.setValue(bindHolder.view, onBindingCallback.get());
            }
            return;
        }
        bindingView.remove(propertyId);
    }
}

到这里基础框架就全部搭建完成了,如果想查看完整代码可通过https://github.com/aopmeta/EasyBinding (opens new window)获取,且其中还包含了对list数据的封装以及demo的演示。