作为超级运费标志着它的两周年纪念,我们回到了绘图板重新设计应用程序.最初的运营商应用程序对拥有一两个司机的所有者运营商来说是成功的,但它没有针对更大的船队进行优化——这是我们直接从客户那里听到的反馈。它可以让承运人从A点找到并将货物转移到B点,但不支持多次装卸的多站装载。
在工程方面,我们的团队规模不断扩大。每一项技术债务都成为我们代码库中另一个不必要的复杂示例。我们的工程速度下降了——简单的功能需要花费数周的时间来部署。效率并没有随着人数的增加而增加。
我们看到了重新设计应用程序作为一个很好的机会来反思我们的应用是如何开发的。在内部,我们希望确保我们团队的总和大于单个部分。从外部来看,我们希望在不影响质量的前提下,迅速为运营商提供新功能,改善用户体验。
Component-driven发展
2016年,Uber的移动平台团队重写了我们的骑手应用在一个新的(现在是开源的)移动架构上肋骨.2017年,Driver团队重写了我们的驱动程序并且采用了同样的架构。
为了确保应用不受未来影响,最初的Uber Freight应用从一开始就使用了rib架构。Uber Freight于2018年5月发布,该应用程序是通过结合多个rib(由路由器、交互器和构建器组成的结构)构建的,这些结构构成了Uber开源、跨平台、移动架构的体系结构。它有很好的代码隔离,一个深思熟虑的肋骨树,深邃的视野,以及良好的关注点分离.
然而,应用结构中的许多rib变得过于庞大和复杂。如果不重构应用的主要部分,就很难共享和重用UI和逻辑的相似部分。
问题在于,Uber Freight应用程序的设计严重依赖于列表。新功能被集成到现有的提要中。List rib的规模和复杂性都在增长,并且变得难以维护。不同的列表通常共享较小的UI和经验片段,但相似的部分埋藏在大的类中,这阻碍了代码重用。
就其本身而言,rib体系结构不够细粒度,无法促进UI和逻辑重用。代码重复成了一个问题,这使得开发变得令人沮丧,实现一致性也变得困难。
为了提高效率和提高开发人员的速度,我们决定构建一个组件驱动的框架来补充现有的rib体系结构。在rib中,UI和逻辑可以被分解成更小的部分,我们的整个应用程序可以由这些独立的、自包含的组件组成。
构建新框架
我们利用四个主要步骤构建了我们的新框架,每个步骤都解决了一个特定的需求:
- 由于应用程序是列表重,我们用RecyclerViews而且UICollectionViews来组成任意UI组件。Uber Freight应用程序主要为承运人及其司机列出装载量。用户可以滚动多个提要,显示新的、当前和过去的负载、负载细节、张贴卡车的入口点并等待匹配的机会、关于货运设施的信息、推荐以及许多其他产品和功能。
- 由于应用程序在多个功能和屏幕上共享一致的设计模式,我们构建了模块化的、可重用的ListView组件。虽然RecyclerViews和UICollectionViews通常用于显示内容的动态列表,但我们选择通过将组件组合在一起来构建动态和静态屏幕。通过这种方式,每个组件都可以在其他特性中轻松重用,而不需要复制UI、表示逻辑或业务逻辑。
- 减少移动复杂性的后端api。虽然Uber Freight应用的原始架构已经采用了服务器驱动的方法,但它需要大量的移动逻辑来处理原始的后端数据。我们从头开始构建了新的api和数据模型,以支持后端数据列表和移动UI组件列表之间更直接的映射,允许客户端代码更多地关注业务和表示逻辑,而不是数据转换。
- 由于新功能可能内置到现有的列表提要中,我们使用插件将后端数据映射到ListView组件时清晰地分离逻辑。与隐藏在菜单和子菜单中的用户体验不同,新功能和产品可以直接在应用程序的主提要中访问。为了防止我们的饲料肋在尺寸和复杂性上膨胀,我们采用了一种工厂模式使用插件将后端数据列表映射到移动组件列表。这种模式阻止我们写作胶水代码以确保列表组件和列表本身之间的清晰分离。
ListView UI框架
为了通过组合共享UI组件来方便构建视图,我们构建了一个轻量级的ListView框架。动态和静态屏幕是用RecyclerViews (Android)和UICollectionViews (iOS)构建的,如下面的图2所示。这个框架的灵感来自于Airbnb的开源环氧树脂库,它允许工程师以简单、声明的方式构建屏幕。
构建通用列表适配器
设置RecyclerViews和UICollectionViews通常需要大量的配置样板。需要一个数据源实现来管理列表中显示的内容,并且需要将列表中不同类型的UI注册到数据源。
一般来说,大多数应用程序都实现了RecyclerView。适配器或UICollectionViewDataSource显示一个或两个不同视图的列表。在最初的Uber Freight应用中,我们有两位数的适配器和数据源实现,每个都支持特定屏幕或功能的必要UI。重复的逻辑越来越多,变得难以维护。实现上的差异让新工程师感到困惑。设计和产品的不一致悄悄进入了应用程序- UI会在不同屏幕的外观上略有不同(例如字体大小,填充和颜色深浅),组件会在不同功能上表现不同(例如,一些按钮缺少点击反馈)。
使用依赖倒置,我们创建了一个可以显示任何UI组件的通用适配器。每个ListView组件都实现了一个公共接口——它知道自己的UI布局,以及如何将该布局绑定到数据上。列表适配器只是保存ListView组件的注册表,并让组件在屏幕上显示自己。
因为我们的新应用程序中的每个屏幕都重新使用可以显示任何组件的相同适配器实现,共享UI和逻辑是微不足道的。任何新的UI都可以通过简单地将新的和现有的ListView组件组合在一起来构建。
将业务逻辑与表示逻辑分离
由于RecyclerViews和UICollectionViews的工作方式,我们在构建ListView组件时必须意识到两种不同类型的逻辑:
- 表示逻辑,呈现屏幕上内容的逻辑。当用户滚动时,列表的部分内容可能在屏幕外,用户不可见。RecyclerViews和UICollectionViews将从屏幕外数据回收视图,并重用它们来显示屏幕上的数据。因此,ListView组件可能需要在一个回收视图中表示它的数据。
- 业务逻辑,还有其他背景逻辑吗?例如,一些组件可能执行网络调用来更新其数据,以便屏幕上显示的内容是最新的。
每种类型的逻辑都需要自己的生命周期。每当组件需要显示在屏幕上时,就会执行表示逻辑。然而,业务逻辑需要单独的生命周期,因为即使组件不在屏幕上,它也可以执行。
Uber Freight应用中的大多数ListView组件只需要表示逻辑。这些组件呈现相对简单的视图。我们构建了ListView框架来支持需要业务生命周期的复杂组件。在Android上,它们被表示为一个不可见的RIB。在iOS上,它们被构建为与RIB生命周期相关的管理器类。
随着我们的提要屏幕的大小和复杂性的增长,所有的业务逻辑- - - - - -单击处理、后台网络调用、路由和启动全新的流都可以由组件本身处理。这可以防止父列表RIB不得不处理它的子组件的所有逻辑。
我们构建ListView框架是为了无缝地补充Uber现有的rib架构。我们选择使用RIB生命周期作为ListView框架中的业务生命周期,这是一个双赢的结果。rib成为框架中至关重要的一部分,已经熟悉rib的开发人员现在可以将它们用作ListView组件。
服务器驱动的呈现
Uber Freight应用一直有服务器驱动的UI呈现,后端api发送数据列表以显示在提要中。后端控制应用程序中的所有排序、排序和内容。
我们希望维护这种服务器驱动的方法,并使用新的组件驱动的UI框架来补充它。我们在最初的Uber Freight应用中遇到的一个问题是,随着功能的增加,移动复杂性以两种方式增长。首先,我们的数据模型将变得相当大,并且有很深的嵌套结构,需要繁琐的移动逻辑将原始数据转换为视图模型。其次,每个新特性都需要额外的转换逻辑。因此,我们的列表rib的规模和复杂性都在增长,这使得它们难以维护和构建。
使用联合创建新的数据模型
在优步,我们使用Apache节俭定义所有数据模型并自动生成每种语言(iOS, Android, Go等)的实现。在最初的Uber Freight应用中,我们使用Thrift类型Struct + enum为我们大多数面向移动的实体建模。对于新的应用程序设计,我们决定使用Thrift联盟广泛使用类型,因为我们发现它是表示具有不同类型的项列表的一种更简洁和准确的方式,这是支持许多基于提要的页面的核心数据结构。联盟它还有其他一些简洁的功能,比如映射到Swift枚举与相关的类型在iOS上使用方便。
联盟是一个结构体在许多可能性中只携带一种类型的字段的抽象。这种抽象允许移动代码作为结构很容易地被使用,而不需要知道具体的类型(类似于客户机接口或协议)。举个例子,a联盟被称为MyShape.
每一个MyShape客户端接收到的要么是矩形,一个三角形,或圆.自动生成的客户端数据模型可以访问确切的类型和字段,并且仍然可以将所有内容视为MyShape模型。
我们小心翼翼地为应用程序中的每个列表提要创建联合数据模型。例如,搜索提要端点返回一个列表SearchCard联盟数据。搜索列表rib不需要知道卡a的具体类型SearchCard是,它可以简单地消耗一个数组吗SearchCards,并将SearchCard对应的移动列表组件。
如果是一个特定的设计组件,就说a矩形,在多个列表提要中使用,我们可以包括矩形结构体在不同的工会里。移动代码可以重用解包和显示数据所需的相同逻辑矩形如果有必要的话)。通过仔细地设计我们的后端以与移动UI相对应,我们减少了客户端代码中的转换逻辑量。
把它们放在一起
通过ListView UI框架,我们有了一个组件驱动的前端架构。有了联合,我们精心设计了返回后端联合模型列表的api。
插件:将工会变成移动组件
为了将后端联合列表转换为移动UI组件列表,我们开始使用来自Uber的RIB架构的插件作为组件工厂。
考虑如下例所示的后端工会:
在这里,我们持有MyUnionPluginPoint与插件工厂注册列表(PluginFactoryA,PluginFactoryB,PluginFactoryC).每个插件工厂只适用于这个联盟中的一个特定的情况。例如,PluginFactoryA处理一个类型,PluginFactoryB处理B类型和PluginFactoryC处理C类型。
如上面的图4所示,List{中的每个后端数据模型一个,B,C}作为输入发送到插件工厂。每个输入的输出是一个移动组件列表—后端联合和列表视图组件之间的一对多映射。例如,单个后端模型可以返回标题组件、消息组件和按钮组件。
这些ListView组件被组合起来,然后显示在RecyclerView或UICollectionView中。
要了解这种抽象如何保持代码库的整洁,请考虑以下示例。如果没有插件,转换代码会变得复杂、难以阅读,并且可能会引入如下所示的错误:
转换的逻辑一个,B,C被隔离在PluginFactory类一个,B,C分别消除了调试大型if/else和switch语句的需要。随着Uber货运工程团队的壮大,代码隔离变得越来越重要。当新工程师的任务是构建功能D,而不必浏览胶水代码影响一个,B,C改善他们的开发体验。
有了新的后端模型、新的前端框架和新的插件作为工厂从一个转换到另一个,Uber Freight应用程序现在是一个良好的,服务器驱动的机器!关于rib、插件和组件驱动的UI如何帮助Uber扩展的另一个例子——包括实验、安全性和更多服务器驱动的优点——请阅读为使用rib的驱动首选项设计一个安全的、可伸缩的、服务器驱动的平台.
新的基金会
最后,我们将重新设计应用作为对未来的全新规划。除了创建一个组件驱动的框架,我们还利用这个机会在更坚实的基础上进行了重建:
- 我们大量添加和回填单元测试(以及iOS上的快照测试)。
- 我们创建了ListView组件的UI库,作为示例应用程序。由于这些组件是我们应用程序的构建模块,我们可以很容易地在图库本身调试生产问题。
- 我们创建了一个巨大的模拟后端数据库,这进一步帮助我们在UI库中调试组件。由于我们的应用程序是服务器驱动的,如果后端不发送组件,过去很难调试特定的UI问题。现在,我们可以模拟任何组件,而不需要触发正确的条件。
- 我们小心翼翼地记录了大量的分析数据,使我们的数据科学团队能够定量地研究和调试如何改善用户体验。雷竞技是骗人的
我们很乐意分享新的Uber Freight应用与运营商和他们的司机,并希望我们在整个过程中的学习证明对其他移动工程师有帮助。
是否有兴趣开发软件来帮助改善承运人的生活,提高运输效率?考虑加入我们的团队!








