8 年老 Android 开发谈;简化 UI 都没弄明白凭什么拿高薪?,android 路由框架
List<Node> attrs = new ArrayList<Node>();Class<? extends View> viewClass; // for view nodesAttributeSetter setter; // for attribute setter nodes
public Node(Class<? extends View> c) {this.viewClass = c;}
public Node(AttributeSetter setter) {this.setter = setter;}}
现在我们需要定义类在产生虚拟布局的时候实际能干的事情了,那就让我们来调用可渲染类吧。一个可渲染类可以是一个 Activity,或者一个自定义的 ViewGroup,或者 Fragment 也凑合。每一个可渲染类都应该有一个用于返回虚拟布局的方法,此外,如果这个方法指定了它将要作用于实际布局中的哪个 View 会更好。
public interface Renderable {Node view();ViewGroup getRootView();}
由于 v() 方法的第一个参数是 View 子类的泛型,所以你不用担心类型安全问题。剩下的参数都是结点类型,所以我们只需要把它们添加到 list 中,无视掉空结点的话效果会更好一些。
public static Node v(final Class<? extends View> cls, final Node ...nodes) {return new Node(cls) ;}
Here's an example of the text() attribute setter (the real code is a bit different, but it could h
ave been implemented like this):
下面是一个 text() 属性的设置方法(实际代码会有点不一样,但是也能像下面这样实现):
public static Node text(final String s) {return new Node(new AttributeSetter() {public void set(View v) {((TextView) v).setText(s);}});}
其他类似的工具方法也能用于改变线性布局的方向,View 的大小、页边距、间距,总之所有 View 的参数都能被改变。
那么,我们要怎么去渲染呢?
现在我们需要一个“渲染者”。这是一个能够根据类名创建 View ,使用 AttributeSetters 修改对应的参数并且递归地添加子 View 的方法。(同样的,下面的代码也是被简化的,实际的代码会有些不一样,主要差别在于当结点没有被改变的时候,我们应该如何避免视图的渲染)
public static View inflateNode(Context c, Node node, ViewGroup parent) {if (node.viewClass == null) {throw new RuntimeException("Root is not a view!");}// Exception handling skipped here to make the code look shorterView v = (View) node.viewClass.getConstructor(Context.class).newInstance(c);parent.addView(v);for (Node subnode: node.attrs) {if (subnode.setter != null) {subnode.setter.set(v);} else {View subview = inflateNode(c, subnode, (ViewGroup) v);}}return v;}
现在我们真的可以摆脱 XMLS,并以一种简洁的方式通过 Java 进行布局了。
布局结点不应该直接地被使用,而应该是通过 render(Renderer r) 和 render()被使用。前者用于重渲染某一个 View,后者用于重渲染所有被展示的 View。Renderer 通过一个弱哈希表存储,使得在 View 被移除或者 Activity 被销毁的同时 - 他们的渲染者也不会再被使用。
什么时候去渲染呢?
这个框架的核心在于 自动进行重渲染,使得 UI 总能展示当前的虚拟布局状态。这就意味着 render() 应该在某个特定的节点被调用。
我参考 Mithril 的方法,把每一个 On...Listener 和 调用 render 的方法捆绑在每一次 UI 的交互中。
public static Node onClick(final View.OnClickListener listener) {return new Node(new AttributeSetter() {public void set(View v) {v.setOnClickListener(new View.OnClickListener() {public void onClick(View v) {listener.onClick(v);// After the click was processed - some data may have been changed// so we try to re-render the UIrender();}});}});}
我觉得这样做是有道理的,因为大多数 Android 应用的数据都是在发生用户交互的时候被改变的。如果你的数据是因为其他因素被改变的 - 那就只能手动通过 render()渲染了。
总的来说
这个方法虽然简单,却非常有用:
你能用类似 XML 的方式定义你的布局结构(通过嵌套调用 v() 方法)
你能用一种清晰易懂的方式绑定数据和监听器
布局都是类型安全的,并且你的编译器会自动完成相应的工作
没有运行时产生的开销,没有使用反射机制,没有自动生成代码
你能在任何地方使用 Java(变量,语句,宏)生成布局
你能用自定义 View 和自定义的属性设置方法
因为你的所有 UI 数据都被保存在属性中,因此你能轻易的保存它们
使用纯 Java 实现这些逻辑需要的代码还不到 250 行!
以上证明了这个方法是可行的。现在我在想,如果有人想要用这个方法开发一个功能齐全的库呢?
设计一个好的“区分”算法会是其中的关键。基本地,它应该能判断一个结点是否被添加/移除/修改,而文件就在于属性节点。简单的数据类型我们只要调用 equals() 去比较两个值就可以了,但是监听器呢?
v(SomeView.java,onClick(v => ...));
这样做的话每一次虚拟树被创建,都会创建一个对应的监听器对象。那怎么去比较它们?还是永远都不更新监听器,只更新发生了改变的监听器类?或者使用某种事件分发机制分发事件,而不是使用监听器?
另一件需要被注意的是:我不想自己把所有属性设置方法写好。这里有一个更好的方法,也就是 Kotlin 他们在 koan 库中做的那样。
我现在在研究怎么从 android.jar 的类中自动生成设置器,以使得这个项目更有用。
分享读者
作者 2013 年 java 转到 Android 开发,在小厂待过,也去过华为,OPPO 等大厂待过,18 年四月份进了阿里一直到现在。
被人面试过,也面试过很多人。深知大多数初中级 Android 工程师,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长,而且极易碰到天花板技术停滞不前!
我们整理了一份阿里 P7 级别的 Android 架构师全套学习资料,特别适合有 3-5 年以上经验的小伙伴深入学习提升。
主要包括阿里,以及字节跳动,腾讯,华为,小米,等一线互联网公司主流架构技术。如果你有需要,尽管拿走好了。
评论