从行程到小部件
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的日常工作之外,我还是旅行和摄影的忠实粉丝。 我喜欢探索一个国家的隐藏瑰宝,而不是通常的旅游景点。