原文链接: https://medium.com/airbnb-engineering/whats-next-for-mobile-at-airbnb-5e71618576ab
What’s Next for Mobile at Airbnb
Airbnb在移动领域的下一步
This is the fifth in a series of blog posts in which we outline our experience with React Native and what is next for mobile at Airbnb.
这是一系列博客文章中的第五篇,其中我们概述了我们在React Native方面的经验以及Airbnb的移动应用程序。
Exciting Times Ahead
Even while experimenting with React Native, we continued to accelerate our efforts on native as well. Today, we have a number of exciting projects in production or in the pipeline. Some of these projects were inspired by the best parts and learnings from our experience with React Native.
激动人心的时刻
即使在尝试使用React Native的同时,我们也继续加快了在原生的工作。 今天,我们有许多令人兴奋的项目正在生产或正在筹备中。 其中一些项目的灵感来自于我们使用React Native的最佳部分和经验。
Server-Driven Rendering
Even though we’re not using React Native, we still see the value in writing product code once. We still heavily rely on our universal design language system (DLS) and many screens look nearly identical on Android and iOS.
Several teams have experimented with and started to unify around powerful server-driven rendering frameworks. With these frameworks, the server sends data to the device describing the components to render, the screen configuration, and the actions that can occur. Each mobile platform then interprets this data and renders native screens or even entire flows using DLS components.
Server-driven rendering at scale comes with its own set of challenges. Here is a handful we’re solving:
- Safely updating our component definitions while maintaining backward compatibility.
- Sharing type definitions for our components across platforms.
- Responding to events at runtime like button taps or user input.
- Transitioning between multiple JSON-driven screens while preserving internal state.
- Rendering entirely custom components that don’t have existing implementations at build-time. We’re experimenting with the Lona format for this.
Server-driven rendering frameworks have already provided huge value by allowing us to experiment with and update functionality instantly over-the-air.
服务器驱动的渲染
尽管我们没有使用React Native,但我们仍然可以在编写产品代码时看到其价值。我们仍然非常依赖我们的通用设计语言系统(DLS),并且在Android和iOS上许多屏幕看起来几乎完全相同。
几个团队已经尝试并开始统一强大的服务器驱动的渲染框架。使用这些框架,服务器将数据发送到描述要呈现的组件的设备,屏幕配置以及可能发生的操作。然后每个移动平台解释这些数据并使用DLS组件呈现本地屏幕或甚至整个流程。
大规模服务器驱动的渲染带来了自己的一系列挑战。这里有一小部分我们正在解决:
在保持向后兼容性的同时安全地更新组件定义。
跨平台共享我们组件的类型定义。
响应按钮点击或用户输入等运行时事件。
在保留内部状态的同时在多个JSON驱动的屏幕之间转换。
在构建时完全渲染没有现有实现的自定义组件。我们正在为此尝试Lona格式。
服务器驱动的渲染框架已经通过允许我们立即实验和更新功能而提供了巨大的价值。
Epoxy Components
In 2016, we open sourced Epoxy for Android. Epoxy is a framework that enables easy heterogeneous RecyclerViews, UICollectionViews, and UITableViews. Today, most new screens use Epoxy. Doing so allows us to break up each screen into isolated components and achieve lazy-rendering. Today, we have Epoxy on Android and iOS.
This is what it looks like on iOS:
BasicRow.epoxyModel(
content: BasicRow.Content(
titleText: "Settings",
subtitleText: "Optional subtitle"),
style: .standard,
dataID: "settings",
selectionHandler: { [weak self] _, _, _ in
self?.navigate(to: .settings)
})
On Android, we have leveraged the ability to write DSLs in Kotlin to make implementing components easy to write and type-safe:
basicRow {
id("settings")
title(R.string.settings)
subtitleText(R.string.settings_subtitle)
onClickListener { navigateTo(SETTINGS) }
}
一个框架组件,就不翻译了
2016年,我们为Android开源Epoxy。 Epoxy是一个框架,可以轻松实现异构RecyclerViews,UICollectionViews和UITableViews。 今天,大多数新屏幕使用Expoxy。 这样做可以让我们将每个屏幕分解为独立的组件并实现懒惰渲染。 今天,我们在Android和iOS上安装了Epoxy。
(代码什么的,看原文吧)
Epoxy Diffing
In React, you return a list of components from render. The key to React’s performance is that those components are just a data model representation of the actual views/HTML you want to render. The component tree is then diffed and only the changes are dispatched. We built a similar concept for Epoxy. In Epoxy, you declare the models for your entire screen in buildModels. That, paired with the elegant Kotlin DSL makes it conceptually very similar to React and looks like this:
override fun EpoxyController.buildModels() {
header {
id("marquee")
title(R.string.edit_profile)
}
inputRow {
id("first name")
title(R.string.first_name)
text(firstName)
onChange {
firstName = it
requestModelBuild()
}
}
// Put the rest of your models here...
}
Any time your data changes, you call requestModelBuild() and it will re-render your screen with the optimal RecyclerView calls dispatched.
On iOS, it would look like this:
override func itemModel(forDataID dataID: DemoDataID) -> EpoxyableModel? {
switch dataID {
case .header:
return DocumentMarquee.epoxyModel(
content: DocumentMarquee.Content(titleText: "Edit Profile"),
style: .standard,
dataID: DemoDataID.header)
case .inputRow:
return InputRow.epoxyModel(
content: InputRow.Content(
titleText: "First name",
inputText: firstName)
style: .standard,
dataID: DemoDataID.inputRow,
behaviorSetter: { [weak self] view, content, dataID in
view.textDidChangeBlock = { _, inputText in
self?.firstName = inputText
self?.rebuildItemModel(forDataID: .inputRow)
}
})
}
}
A New Android Product Framework (MvRx)
One of the most exciting recent developments is a new Framework we’re developing that we internally call MvRx. MvRx combines the best of Epoxy, Jetpack, RxJava, and Kotlin with many principles from React to make building new screens easier and more seamless than ever before. It is an opinionated yet flexible framework that was developed by taking common development patterns that we observed as well as the best parts of React. It is also thread-safe and nearly everything runs off of the main thread which makes scrolling and animations feel fluid and smooth.
So far, it has worked on a variety of screens and nearly eliminated the need to deal with lifecycles. We are currently trialing it across a range of Android products and are planning on open sourcing it if it continues to be successful. This is the complete code required to create a functional screen that makes a network request:
data class SimpleDemoState(val listing: Async<Listing> = Uninitialized)
class SimpleDemoViewModel(override val initialState: SimpleDemoState) : MvRxViewModel<SimpleDemoState>() {
init {
fetchListing()
}
private fun fetchListing() {
// This automatically fires off a request and maps its response to Async<Listing>
// which is a sealed class and can be: Unitialized, Loading, Success, and Fail.
// No need for separate success and failure handlers!
// This request is also lifecycle-aware. It will survive configuration changes and
// will never be delivered after onStop.
ListingRequest.forListingId(12345L).execute { copy(listing = it) }
}
}
class SimpleDemoFragment : MvRxFragment() {
// This will automatically subscribe to the ViewModel state and rebuild the epoxy models
// any time anything changes. Similar to how React's render method runs for every change of
// props or state.
private val viewModel by fragmentViewModel(SimpleDemoViewModel::class)
override fun EpoxyController.buildModels() {
val (state) = withState(viewModel)
if (state.listing is Loading) {
loader()
return
}
// These Epoxy models are not the views themself so calling buildModels is cheap. RecyclerView
// diffing will be automaticaly done and only the models that changed will re-render.
documentMarquee {
title(state.listing().name)
}
// Put the rest of your Epoxy models here...
}
override fun EpoxyController.buildFooter() = fixedActionFooter {
val (state) = withState(viewModel)
buttonLoading(state is Loading)
buttonText(state.listing().price)
buttonOnClickListener { _ -> }
}
}
MvRx has simple constructs for handling Fragment args, savedInstanceState persistence across process restarts, TTI tracking, and a number of other features.
We’re also working on a similar framework for iOS that is in early testing.
Expect to hear more about this soon but we’re excited about the progress we’ve made so far.
Iteration Speed
One thing that was immediately obvious when switching from React Native back to native was the iteration speed. Going from a world where you can reliably test your changes in a second or two to one where may have to wait up to 15 minutes was unacceptable. Luckily, we were able to provide some much-needed relief there as well.
We built infrastructure on Android and iOS to enable you to compile only part of the app that includes a launcher and can depend on specific feature modules.
On Android, this uses gradle product flavors. Our gradle modules look like this:
1_KVrbsdwESyfbtKFeh2acXg.pngThis new level of indirection enables engineers to build and develop on a thin slice of the app. That paired with IntelliJ module unloading dramatically improves build and IDE performance on a MacBook Pro.
We have built scripts to create a new testing flavor and in the span of just a few months, we have already created over 20. Development builds that use these new flavors are 2.5x faster on average and the percentage of builds that take longer than five minutes is down 15x.
For reference, this is the gradle snippet used to dynamically generate product flavors that have a root dependency module.
Similarly, on iOS, our modules look like this:
1_AVB7em_JCmj-JmjTCkLdQw.pngThe same system results in builds that are 3–8x faster.
Conclusion
It is exciting to be at a company that isn’t afraid to try new technologies yet strives to maintain an incredibly high bar for quality, speed, and developer experience. At the end of the day, React Native was an essential tool in shipping features and giving us new ways of thinking about mobile development. If this sounds like a journey you would like to be a part of, let us know!
This is part five in a series of blog posts highlighting our experiences with React Native and what’s next for mobile at Airbnb.
如有侵权,请立刻告知。
网友评论