☰
简化 Android 的 UI 开发:基于虚拟布局与自动重渲染的纯 Java 数据绑定方案
2026/10/10 5:25:18 网站建设 项目流程
  • 文档
  • 教程
  • 知识库

【免费下载链接】android-tech-frontier

【停止维护】一个定期翻译国外Android优质的技术、开源库、软件架构设计、测试等文章的开源项目

项目地址:https://gitcode.com/gh_mirrors/an/android-tech-frontier
点击查看免费下载

本文依据 android-tech-frontier 仓库 中收录的译文《简化Android的UI开发》(原文作者 Zaitsev Serge,译者 chaossss,校对 ZhaoKaiQiang)整理扩写。文中介绍了一种把 Web 端「虚拟 DOM」思想移植到 Android 的方案:用嵌套的v()调用以纯 Java 声明布局、以属性设置结点绑定数据与监听器、在用户交互后自动重渲染差异部分,从而摆脱臃肿脆弱的 XML 布局。读完本文,你将理解这一方案的完整数据结构、渲染流程与自动重渲染机制,并能基于文中不到 250 行的核心思路设计自己的声明式 UI 框架。

一、Android UI 开发为什么如此痛苦

在 Android 上写 UI,代码往往是支离破碎的——大量模板化代码、没有结构可言。原文作者(Zaitsev Serge,其博客文章《Android UI development made easy》)开篇就列出了几个纯属个人见解的问题:

  • Android UI 开发很少符合 MVC(或 M-V-任何其他东西)模式:Activity/Fragment 既要管业务逻辑又要管视图细节,职责混杂;
  • XML 文件包含大量重复代码,代码复用性差:相似的布局要反复复制粘贴,抽成 include、style 的代价又很高;
  • XML 非常脆弱:写错了控件名(例如把TextView打成TextVeiw),编译期编译器不会报任何警告,直到 App 运行到该布局时才会抛出InflateException;
  • 缺少对 styles 的支持,缺少对变量的支持,不支持宏和计算结果(例如10dp + 2px这种表达式无法在 XML 中直接书写);
  • 没有数据绑定:必须自己把所有的findViewById和setOn...Listener一个个写出来;
  • 用 Java 代码直接构造布局:虽然可行,但写出来的代码「有如天书」,冗长且难以阅读。

简而言之:定义布局在一个目录,使用布局在另一个目录,再在 UI 代码里手工改变视图状态——这样的开发方式既不安全、也不高效。

二、Web 端的启示:从 jQuery 到虚拟 DOM 与 Mithril.js

面对同样的问题,Web 开发者们早就开始了自救。在缺少 MVx 框架时开发复杂应用非常吃力,于是大家意识到 jQuery 式「拿到元素、逐个改属性」的写法存在结构性缺陷,先后催生了 Backbone、Knockout、Angular、Ember 等框架。

而 Android 上的常见做法,与 jQuery 时代几乎如出一辙——只是把选择器换成了findViewById:

// Web 的 jQuery 写法 $('.myview').text('Hello'); $('.myview').on('click', function() { ... }); // Android 的传统写法 myView.setText("Hello"); myView.setOnClickListener(new View.OnClickListener() { ... });

随后,React.js 给 Web 开发带来了一个关键转折:以树状关系的自定义对象创建「虚拟 DOM」来描绘实际的 HTML 布局。虚拟树创建和切换的开销都很小,当实际 DOM 需要被渲染时,框架对比「前一棵虚拟树」与「新的虚拟树」,只把不匹配的部分渲染出来。

Mithril.js 则是一个精悍、短小的框架,它让 React 的思路变得更整洁:除了纯 JavaScript,几乎摆脱一切框架束缚,同时让你在写布局时能享受到「图灵完备语言」带来的力量:

return m('div', m('p', someText), m('ul', items.map((item) => m('li', item))), m('button', {onclick: myClickHandler}));

用这样的写法,你能用循环生成许多 View,能用条件语句改变布局中的某个部分,最后还能绑定数据和设置事件监听器。那么——这个方法能否被移植到 Android 中?这正是本文后续要回答的问题。

三、虚拟布局:把虚拟 DOM 的思想搬到 Android

虚拟布局(virtual layout)借鉴了 Web 端虚拟 DOM 的概念:它是一棵由自定义 Java 对象组成的树,用于描述实际的 Android 布局,而不是直接操作真实的 View。

工作流程是:无论 App 数据改变多少次,树都会被重新构建;但最终真正变化的布局内容,应该仅仅是前后两棵树不一致的部分(当前布局与改变前布局的差异)。

作者设计框架时只导入一个静态类,因此所有静态方法都可以不带类名前缀直接使用(例如直接用v()而不是Render.v())——这是利用 Java 静态导入的语言特性带来的书写便利。下面是如何创建布局的示例:

v(LinearLayout.class, orientation(LinearLayout.VERTICAL), v(TextView.class, text(someText)), v(Button.class, text("Click me"), onClick(someClickHandler)));

这里的第一个v()方法返回一个虚拟布局结点;每一次调用后,它返回的是当前应用状态的展示(注意:不是实际的 View!)。

当某个文字变量被改变时,虚拟树中对应的结点值发生变化,框架在下一次渲染时针对这个差异调用setText()更新相应的 TextView 实例,而其余的布局不发生任何变化——这正是「只渲染差异部分」的核心价值。

四、核心数据结构:Node 与 AttributeSetter

一棵虚拟布局树在理想情况下应该只有一种类,作者把它称为结点(Node)。但结点主要有两种类型:

  1. View 结点——对应TextView.class等真实 View 类;
  2. 属性设置结点——例如text(someText),负责对某个 View 设置属性。

这意味着结点需要能「任意」地包含一个 View 类,以及一个用于改变 View 属性的方法。对应的最小实现如下:

interface AttributeSetter { public void set(View v); } public static class Node { List<Node> attrs = new ArrayList<Node>(); Class<? extends View> viewClass; // for view nodes AttributeSetter setter; // for attribute setter nodes public Node(Class<? extends View> c) { this.viewClass = c; } public Node(AttributeSetter setter) { this.setter = setter; } }

从源码结构看,Node是一个高度自洽的树结点:attrs保存子结点列表(既可以是子 View 结点,也可以是属性设置结点),viewClass标记 View 类型,setter则封装「如何修改一个 View」。这样的设计让一棵树可以同时承载结构与行为。

4.1 Renderable:谁拥有这棵虚拟布局树

有了 Node 还不够,还需要定义「产生虚拟布局」的载体。作者将其称为可渲染类(Renderable)——它可以是一个 Activity、一个自定义的 ViewGroup,甚至是一个 Fragment。每一个可渲染类都应该提供一个返回虚拟布局的方法,最好还能指明该方法作用于实际布局中的哪个 View:

public interface Renderable { Node view(); ViewGroup getRootView(); }

4.2 v():声明式 API 的入口

由于v()的第一个参数是 View 子类的泛型(Class<? extends View>),类型安全性得到了保证;其余参数都是结点类型,实现时只需要把它们添加到子结点列表中——如果遇到空结点,直接忽略会更好:

public static Node v(final Class<? extends View> cls, final Node ...nodes) { return new Node(cls) ; }

4.3 属性设置器:text() 的示例实现

下面是text()属性设置器的示例(原文注明:实际代码会略有差异,但完全可以按这样的思路实现):

public static Node text(final String s) { return new Node(new AttributeSetter() { public void set(View v) { ((TextView) v).setText(s); } }); }

其他类似的工具方法也能用于改变线性布局的方向、View 的大小、页边距、间距——总而言之,所有 View 可配置的参数都能被封装成这样的属性设置结点。这也是整个框架可扩展性的根基:每新增一个工具方法,就等于为声明式布局语言新增一个「关键字」。

五、渲染器 inflateNode:把虚拟树变成真实 View

接下来需要一个「渲染者」:它能够根据类名创建 View,使用AttributeSetter修改对应参数,并递归地添加子 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 shorter View 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; }

这段代码揭示了渲染的完整调用链:

  1. 检查根结点确实是 View 结点(viewClass非空),否则直接抛出RuntimeException;
  2. 通过「带 Context 的构造器」反射式地newInstance创建真实 View(原文简化了异常处理,真实工程中需要补充NoSuchMethodException、InstantiationException等处理);
  3. 把新 View 加入父容器parent;
  4. 遍历子结点:若是属性设置结点则调用setter.set(v)修改属性;若是 View 结点则递归inflateNode挂载子视图;
  5. 返回创建出的 View。

到这里,我们已经真正摆脱 XML,并以一种简洁的方式通过 Java 完成布局。需要注意两点(原文中的简化点):

  • 布局结点不应该被直接使用,而应通过render(Renderer r)(重渲染某个 View)和render()(重渲染所有被展示的 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 UI render(); } }); } }); }

这样的设计是有道理的:大多数 Android 应用的数据,都是在用户交互发生时被改变的。点击事件处理完毕后数据可能已经变化,此时调用render()重渲染差异部分即可让 UI 与数据保持一致;而如果你的数据是因其他因素(网络回调、后台线程等)改变的,则只能手动调用render()。

结合前文对 MVVM 模式 的介绍可以看出,这种「交互 → 数据变化 → 自动刷新视图」的闭环,实际上已经触及了数据驱动 UI 的思想,只是它用「事件处理完毕后自动重渲染」来实现,而非依赖官方的可观察字段机制。

七、总的来说:这个方法可行吗?

作者总结道:这个方法虽然简单,却非常有用:

  • 你能用类似 XML 的方式定义布局结构——通过嵌套调用v()方法;
  • 你能用一种清晰易懂的方式绑定数据和监听器;
  • 布局是类型安全的,你的编译器会自动完成相应的工作,不再有运行期InflateException的隐患;
  • 没有运行时产生的开销,没有使用反射机制,没有自动生成代码(注意:inflateNode中的newInstance是创建 View 实例的必要手段,框架本身并未依赖反射做属性绑定);
  • 你能在任何地方使用 Java(变量、语句、宏)生成布局——循环、条件分支、函数复用都是天然能力;
  • 你能使用自定义 View和自定义的属性设置方法;
  • 因为所有 UI 数据都被保存在属性中,因此你能轻易地保存它们(便于状态恢复);
  • 使用纯 Java 实现这些逻辑需要的代码还不到 250 行!

正是这种极低的实现成本,证明了这个方法是可行的。作者在文末直言:现在我在想,如果有人想要用这个方法开发一个功能齐全的库呢?

八、走向完整库:区分算法与自动生成设置器

要把这个原型发展成功能齐全的库,作者指出了两个关键难点。

8.1 设计一个好的「区分(diff)」算法

自动重渲染的核心在于判断一个结点是否被添加 / 移除 / 修改,难点集中在属性结点上:

  • 对于简单的数据类型,调用equals()比较两个值即可;
  • 但监听器(Listener)怎么办?
v(SomeView.java, onClick(v => ...));

每一次虚拟树被创建,onClick都会创建一个全新的监听器对象——两棵树中的监听器永远不会equals()。于是必须回答:怎么去比较它们?还是永远不更新监听器、只更新那些确实发生了改变的监听器类?抑或干脆放弃监听器,改用某种事件分发机制(例如事件总线 / 事件流)来解耦?

这些设计取舍,直接决定了一个声明式 UI 框架在「对比差异」环节的健壮性与性能表现。

8.2 不想手写所有设置器:借鉴 Kotlin Koan

另一个现实问题是:作者不想自己把全部属性设置方法一个个写出来。更好的思路是像 Kotlin 的Koan库那样做——用语言/工具特性批量生成。作者明确表示正在研究如何从 android.jar 的类中自动生成设置器,以让这个项目更有用(从项目本身的设计看,这可以理解为:解析android.jar中各 View 的 setter 签名,自动生成对应的Node静态工厂方法,从而避免手工维护数百个工具方法)。

作者将当时的全部代码以MIT 许可开源为 Anvil 库,并欢迎大家评论和提交 PR——在 Android 官方 Data Binding 尚未成熟的那个年代,这属于社区对「声明式 UI」的早期探索。

九、与官方 Data Binding 的呼应:殊途同归

值得注意的是,这个仓库同期收录了多篇关于 Android 官方数据绑定框架的文章,恰好构成一组完整的对照阅读材料:

  • Android双向数据绑定:介绍利用官方 Data Binding 让两个EditText绑定同一 bean、输入实时互显的场景,属于「双向绑定」;
  • Android数据绑定-再见Presenter,你好ViewModel:讲述 Data Binding 如何接管 Presenter 的职责,推动架构从 MVP 走向 MVVM;
  • 数据绑定(Data Binding)-Part1-Part1.md) 到 Part5-Part5.md):系统讲解官方数据绑定框架的使用与原理。

对比之下可以看到两条路线殊途同归:官方 Data Binding 通过XML 中的@{...}表达式在编译期生成绑定代码(需要编译期代码生成);而本文的方案则把布局与绑定全部收拢到纯 Java,用虚拟树 diff 在运行期完成增量更新,把布局编写重新交还给图灵完备的语言。两者要解决的是同一个问题——UI 与数据状态的一致性与可维护性——只是实现哲学不同。

结语

本文完整还原了「虚拟布局」这一 Android UI 声明式方案的思路:以Node树描述布局、以AttributeSetter封装属性变更、以inflateNode递归实例化真实 View、以交互监听器触发自动重渲染,全程不超过 250 行 Java 代码。这套思想后来也在各类声明式 UI 库中不断得到印证:把「界面 = 状态到视图的映射」作为核心抽象,把 diff 交给框架,把开发者从findViewById与 XML 解析的泥潭中解放出来。

关联原文译文见 others/简化Android的UI开发/readme.md;仓库还收录了 MVVM 模式简介 与官方 Data Binding 系列-Part1.md),可配合阅读,从架构模式与官方实现两个维度理解 Android UI 数据驱动化的演进。

  • 文档
  • 教程
  • 知识库

【免费下载链接】android-tech-frontier

【停止维护】一个定期翻译国外Android优质的技术、开源库、软件架构设计、测试等文章的开源项目

项目地址:https://gitcode.com/gh_mirrors/an/android-tech-frontier
点击查看免费下载

相关推荐

上一篇:XiangShan XSPdb 批处理执行机制:从 CLI 参数、脚本重放到波形回调的自动化调试实现
下一篇:mixwith.js API 完全解析:从 apply() 到 mix().with() 的完整指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询