从行程到小部件

Skyscanner应用程序动态结果页面的故事

由Zsombor Fuszenecker

到目前为止的故事…

在Skyscanner,我们会不断地在我们的应用程序上进行迭代,以帮助旅行者找到适合您旅行的最佳航班,酒店或租车服务。 毫不奇怪,搜索结果页面是应用程序最重要的部分之一。 这是我们的用户将大部分时间用于比较结果和更改搜索参数的地方。

最初,航班搜索结果页面仅用于显示航班行程。 但是,在发布具有以下结果列表的新版本的应用程序后,我们意识到可以改进来自同一提供商的具有多个选项的路由。

请注意,上面的结果非常相似(相同的承运人,价格相差不大,飞行时间略有不同),并且像这样的列表在小屏幕上显示有大量物品并不容易浏览。 我们的目的是使比较变得简单,因此我们最近修改了此页面的外观,以确保比较飞行选项在尽可能小的屏幕上进行浏览。

我们迅速从了解问题转向研究可行的解决方案,然后迭代许多原型。 最后,我们认为我们找到了最佳解决方案:我们没有显示具有很多直航的航线的几乎相同的航班列表,而是按航空公司对列表进行了分组。

通过这样的概述,可以很容易地比较直接承运人及其价格。 我们称这个额外的内容为小部件

随着时间的流逝,我们想要添加到结果列表中的事物数量越来越多。 警告消息,推荐小部件和赞助广告只是结果列表中许多其他项目的一部分。

每次我们添加新类型的内容时,更改应用程序的代码都不是一件容易的事。 有一个巨大的文件,其中包含许多条件和边缘案例处理逻辑。 我们还想进行试验,这意味着我们的数据源甚至充满了条件。 创建新的小部件时,开发人员必须经过数百行的If语句。 没有我们的全力支持,一切都容易被打破,其他团队也无法做出贡献。

因此,在意识到我们无法在结果列表上进行足够快的迭代之后,我们开始计划在其背后的新架构。

设计目标:

  • 快速迭代新的小部件并使其更容易进入列表
  • 最小化创建新小部件会破坏另一个小部件的风险
  • 使实验变得容易; 添加新的小部件应该不难,也不需要列表后面的团队的全面支持。
  • 可以并行或顺序运行小部件计算代码。
  • 使列表在后端可配置,以便独立于应用发布周期发布

我们称该项目为“ Widgetify”。

从构思到生产:

我们的第一步只是绘图。 我们自己坐下来,做了一些建筑草图。 然后我们提出了自己的想法,并达成了团队共识。

在初步感觉到“它在理论上可行”之后,我们创建了Feature标志并将其推到我们的主分支(尽管我们并未在生产中启用该功能,甚至在内部也未启用)。

然后,我们创建了基类,其中第一个调用端口是显示默认内容的列表。 我们能够对列表进行过滤和排序,所有内容(甚至分析)都可以像以前一样工作。 在此阶段,我们默认情况下在内部启用此功能,以便尽快捕获错误

接下来,我们创建了一些虚拟小部件,并确定了该项目可以证明未来。

有时,我们发现一个旧的小部件无法在新平台上正常工作,因此我们不得不关闭该功能并修复该特定的小部件提供商(在Skyscanner,我们现在每个平台执行2周的发布周期)。

最终,当我们觉得有东西要向其他人展示时,我们开始在内部推广该平台并收集其他团队的反馈。 为此,我们在团队旁边坐了三天,帮助他们创建自己的内容。 作为回报,我们获得了宝贵的反馈,并且基于集体反馈,我们反复进行以使构建新内容变得更加容易。

卡和提供者

Widgetify的工作方式如下:我们要在列表中显示的所有新内容(卡片)类型都必须具有provider 。 我们可以基于功能标记注册此提供程序以启用内容类型。 但是提供商并不总是需要退回卡。 例如,如果您是美国公民或永久居民,或者您位于美国境内,则只有在您的旅行符合美国政府批准的十二个类别之一的情况下,才可以前往古巴。 这是法律要求。 我们在每次搜索时都会启动Cuban警告提供程序 ,但是,只有在搜索条件和用户符合条件时,它才会返回一张卡片。

此外,提供程序是可链接的,并且可以依赖其他提供程序。 您可以注册一个仅在另一个提供者之后启动的提供者。

1新搜索首先触发PlaceholderProvider (其工作是创建四个占位符卡)。 占位符提供程序完成后, ItineraryProvider将向我们的服务器发出请求。 我们还设置了AdvertCardProvider ,将其链接到占位符提供程序之后。 这也与行程提供者同时开始。

2行程提供商从服务器获取结果后,我们将创建卡的视图模型。 我们有一个名为TimetableCardProvider的特殊提供程序,用于显示直接航班的摘要。 如果一条路线有很多直航, 那么它将创建一个视图模型并将其返回。 但是,目前,我们没有足够的直接航班供提供者创建此视图模型,因此此刻未显示。

3几秒钟后,我们从服务器接收了更新的行程,并再次触发了TimetableCardProvider 。 这次计算完成,并产生一个新的卡片视图模型,该模型最终出现在列表的前面。

5最后, AdvertCardProvider完成并返回广告卡。 这将放置在第四位置,因此当用户向下滚动时,他们将在那里看到它。

添加新内容有多容易?

假设您要创建一个显示一些HTML内容的新窗口小部件。 例如,这对于成长/营销团队很有用,因为他们可以轻松地向您的应用添加内容。 我们将新的提供程序称为HTMLCardProvider。

该HTML提供程序可以使用服务器端定义的任何动态内容创建窗口小部件。 一个具体的示例可能是一个小部件,该小部件突出显示一周中的几天,当前搜索可在该天进行直接飞行。 在当天点击时,用户将能够设置搜索参数。

为此,您需要创建三个文件:

HTMLCardProvider

  #import“ HTMLCardProvider.h” 
  #import“ HTMLCardViewModel.h” 

  @implementation HTMLCardProvider 
  { 
  SKYHtmlWidgetClient * _htmlClient; 
  } 
  -(instancetype)init 
  { 
 如果(自我= [super initWithType:SKYFlightsDayviewPipelineItemTypeCustomWebView父对象:SKYFlightsDayviewPipelineItemTypePlaceholder]) 
  { 
  _htmlClient = [SKYHtmlWidgetClient新]; 
  } 
 返回自我 
  } 
  -(无效)开始 
  { 
  [[self getServiceResponse] continueWithBlock:^ id _Nullable(BFTask  * _Nonnull任务) 
  { 
  //如果响应成功,则进入此阶段。 发生错误时,该块将不会运行。 
  SKYHTMLWidgetServiceResponse * serviceResponse = task.result; 
  HTMLCardViewModel * htmlViewModel = [[CustomWebViewCardViewModel alloc] initWithData:serviceResponse.data]; 
  self.finished = YES; 
  [self.delegate提供者:self refreshCompletedWithModels:@ [htmlViewModel]]; 
  } 
  } 
  -(void)registerCellForCollectionView:(UICollectionView *)collectionView 
  { 
  [CellViewWithReuseIdentifier:[HTMLCardViewModel cellName]的[collectionView registerClass:[HTMLCardViewModel cellClass]]; 
  } 
  @结束 

HTMLCardViewModel:

ViewModel包含后端呈现的html和单元格的高度。

HTMLCardCell:

  #import“ HTMLCardCell.h” 
  @implementation HTMLCardCell 
  { 
  UIWebView * _webview; 
  } 
  -(instancetype)initWithFrame:(CGRect)frame 
  { 
 自我= [super initWithFrame:frame]; 
 如果(自己) 
  { 
  _webview = [[UIWebView分配] initWithFrame:CGRectMake(0,0,frame.size.width,frame.size.height)]; 
  _webview.scrollView.scrollEnabled =否; 
  _webview.scrollView.bounces =否; 
  _webview.delegate =自我; 
  [self.contentView addSubview:_webview]; 
  } 
 返回自我 
  } 
  -(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)请求导航类型:(UIWebViewNavigationType)navigationType { 
  NSURL * URL = [请求URL]; 
 如果([[URL scheme] isEqualToString:@” skyscanner://“]){ 
  //深度链接到适当的页面 
  [[UIApplication sharedApplication] openURL:[NSURL URLWithString:[URL absoluteString]]]]; 
 返回否; 
  } 
 返回是; 
  } 
  #pragma mark —布局 
  -(void)setViewModel:(HTMLCardViewModel *)viewModel 
  { 
  _viewModel = viewModel; 
  [_webview loadHTMLString:_viewModel.htmlString baseURL:nil]; 
  } 

  -(void)viewDidLayoutSubviews 
  { 
  [super viewDidLayoutSubviews]; 
  [_webview setFrame:CGRectMake(0,0,self.frame.size.width,self.frame.size.height)]; 
  } 
  @结束 

结论

在该项目于11月开始之前,我们有3个“小部件”,而增加第四个则要花一个开发人员至少一周的时间。 今天,我们有来自三个团队的14个提供程序,其中一个是可以从后端配置的HTML提供程序。 这意味着Web开发人员可以在没有我们全力支持的情况下更轻松地移植其功能。

我们还使用该平台创建带有原型的示例应用程序,因此我们可以与全球用户一起迅速尝试新的想法,以验证我们未来功能的用户体验。

此外,现在打破应用程序的基本功能变得更加困难,并且在客户端和服务器端,动态配置(例如,功能标记)也更加容易。

 如果([FeatureFlag isHtmlWidgetEnabled]) 
  { 
  //创建提供者 
  //注册提供商 
  } 

CardProvider将尝试获取数据,但如果服务器未返回数据,它将不会创建ViewModel。 将来,我们只会在应用程序中添加终止开关功能标志,因此后端可以告诉应用程序是否应显示内容。

将此功能与后端后端服务相结合,在不更改应用程序代码的情况下,在结果页上进行实验和尝试新功能要容易得多。 因此,我们不需要经过漫长的App Store审核过程,后端可以随时提供新的小部件,而新的小部件将在数分钟而不是数周内生效。

喜欢你听到的吗? 跟我们工作

我们在Skyscanner上的工作方式有所不同,我们正在寻找遍布我们全球办事处的更多工程部落成员。 请查看我们的Skyscanner职位以获取更多职位空缺。

关于作者

我叫Zsombor Fuszenecker,我在布达佩斯办公室使用Skyscanner应用程序。 目前,我们的小组负责结果和预订页面。 我们旨在帮助旅行者更轻松地找到他们喜欢的航班。 看到我们的用户使用并推荐我帮助构建的功能后,我会感到很兴奋。

除了在Skyscanner的日常工作之外,我还是旅行和摄影的忠实粉丝。 我喜欢探索一个国家的隐藏瑰宝,而不是通常的旅游景点。