Перевод статьи Gabriel Peal: What’s Next for Mobile at Airbnb.
Это пятая статья в серии, в которой мы поделимся нашим опытом с React Native и расскажем, что ждёт в дальнейшем мобильную разработку в Airbnb.
Даже экспериментируя с React Native, мы продолжали наращивать наши усилия и в области нативной разработки. Сегодня у нас есть ряд интересных проектов в продакшене или в производстве. Некоторые из этих проектов были вдохновлены опытом работы с React Native.
Несмотря на то, что мы не используем React Native, мы по-прежнему видим ценность в написании кода продукта единожды. Мы по-прежнему сильно полагаемся на нашу систему универсального языка дизайна (DLS, и многие экраны выглядят почти идентичными на Android и iOS.
Несколько команд экспериментировали и начали объединяться вокруг мощных фреймворков серверного рендеринга. С помощью этих фреймворков сервер отправляет данные на устройство, описывающие компоненты для визуализации, конфигурацию экрана и действия, которые могут произойти. Затем каждая мобильная платформа интерпретирует эти данные и отображает собственные экраны или даже целые пользовательские сценарии с помощью DLS-компонентов.
Управляемый сервером рендеринг поставляется с собственным набором проблем. Вот горстка, которую мы решаем:
- Безопасное обновление определений компонентов при сохранении обратной совместимости.
- Совместное использование определений типов для наших компонентов на разных платформах.
- Реагирование на события во время выполнения, такие как нажатие кнопок или пользовательский ввод.
- Переход между несколькими экранами, управляемыми JSON, с сохранением внутреннего состояния.
- Визуализация полностью настраиваемых компонентов, которые не имеют существующих реализаций во время сборки. Мы экспериментируем с форматом Lona для этого.
Фреймворки серверного рендеринга уже предоставили огромную ценность, позволяя нам экспериментировать и мгновенно обновлять функциональность без обновления приложения у пользователя.
В 2016 году мы выложили в опенсорс Epoxy для Android. Epoxy — это фреймворк, который делает доступными гетерогенные RecyclerViews, UICollectionViews и UITableViews. Сегодня большинство новых экранов используют Epoxy. Это позволяет разбить каждый экран на отдельные компоненты и добиться ленивого рендеринга. Сегодня у нас есть Epoxy на Android и iOS.
Вот как это выглядит на iOS:
BasicRow.epoxyModel(
content: BasicRow.Content(
titleText: "Settings",
subtitleText: "Optional subtitle"),
style: .standard,
dataID: "settings",
selectionHandler: { [weak self] _, _, _ in
self?.navigate(to: .settings)
})
https://gist.github.com/gpeal/93452a45351ddabf2b06e12cca7271b9#file-documentmarquee-swift
На Android мы использовали возможности DSL в Kotlin, чтобы сделать реализации компонент простыми для написания и типобезопасными:
basicRow {
id("settings")
title(R.string.settings)
subtitleText(R.string.settings_subtitle)
onClickListener { navigateTo(SETTINGS) }
}
https://gist.github.com/gpeal/94cc2a65cfdc00ed1283329f614c98e1#file-epoxy-kt
В React вы возвращаете список компонентов из функции рендера. Ключ к производительности React заключается в том, что эти компоненты являются просто моделью данных фактических представлений/HTML, которые вы хотите отобразить. Затем дерево компонентов сравнивается и отправляются только изменения. Мы создали подобную концепцию для Epoxy. В Epoxy модели объявляются для всего экрана в buildModels. Это, в сочетании с элегантным Kotlin DSL делает его концептуально очень похожим на React и выглядит так:
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...
}
https://gist.github.com/gpeal/45094cb71ebe1bf943cfd20b10c65227#file-simpleepoxycontroller-kt
Каждый раз, когда ваши данные изменяются, вы вызываете функцию requestModelBuild()
и она перерисовывает ваш экран с оптимальными обработками вызовов RecyclerView.
На iOS, это будет выглядеть так:
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)
}
})
}
}
https://gist.github.com/gpeal/fb032ca2ee20a3d2541d369101356fca#file-epoxy-swift
Одно из самых ярких последних событий — это новая платформа, которую мы развиваем, внутри мы называем её MvRx. MvRx совмещает самое лучшее от Epoxy, Jetpack, RxJava, и Kotlin со множеством принципов React для того чтобы сделать создание новых экраны более легким и более бесшовным чем когда-либо. Это самоуверенный, но гибкий фреймворк, который был разработан на основе общих паттернов разработки, которые мы наблюдали, а также берёт лучшие части React. Он также потокобезопасен, и почти все работает в отдельном потоке от основного, что делает прокрутку и анимацию плавной и гладкой.
До сих пор он работал на различных экранах и почти исключил потребность работать с жизненными циклами. В настоящее время мы оцениваем его по целому ряду продуктов на Android и планируем открыть исходный код, если результат будет успешным. Это полный код, необходимый для создания функционального экрана, который делает сетевой запрос:
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 { _ -> }
}
}
https://gist.github.com/gpeal/87099abc09503f2e14a1c31b7bdcdc06#file-simpledemo-kt
MvRx имеет простые конструкции для обработки Fragment args
, сохраняет savedInstanceState при перезапуски процесса, отслеживает TTI, и содержит ряд других особенностей.
Мы также работаем над аналогичной платформой для iOS, которая находится в раннем тестировании.
Вы можете услышать больше об этом в самое ближайшее время, и мы рады прогрессу, которого мы достигли.
Одна вещь, которая была сразу очевидна при переключении с React Native обратно в нативную разработку — это скорость итерации. Переход из мира, где вы можете надежно проверить свои изменения за секунду или две к миру, где, возможно, придется ждать до 15 минут, был неприемлемым. К счастью, мы также смогли оказать необходимую помощь.
Мы построили инфраструктуру на Android и iOS, которая позволить скомпилировать только часть приложения.
На Android это сделано при помощи gradle product flavors. Наши модули gradle выглядят так:
Этот новый уровень косвенности позволяет инженерам работать с тонким срезом приложения. В сочетании с выгрузкой модулей в IntelliJ это значительно улучшает производительность сборки и IDE на MacBook Pro.
Мы написали скрипты для создания нового тестируемого flavor и всего за несколько месяцев мы уже создали более 20. Сборки для разработки, использующие эти новые flavor, в среднем в 2,5 раза быстрее, а процент сборок, которые занимают больше пяти минут, в 15 раз меньше.
Для справки, это фрагмент gradle-кода, используемый для динамического создания разновидностей приложения с корневой зависимостью.
Аналогично, на iOS наши модули выглядят так:
Те же результаты в сборках, которые теперь проходят в 3 — 8 раз быстрее.
Интересно находиться в компании, которая не боится пробовать новые технологии, но стремится поддерживать невероятно высокую планку для качества, скорости и опыта разработчика. В конце концов, React Native был важным инструментом для быстрой доставки нового функционала и дал нам новые способы мышления о мобильной разработке. Если это звучит как путешествие, частью которого вы хотели бы быть, дайте нам знать!
Это пятая часть в серии статей, освещающих наш опыт работы с React Native в Airbnb.
Часть 1: React Native в Airbnb
Часть 2: Технология
Часть 3: Создание кроссплатформенной мобильной команды
Часть 4: Принятие решения по React Native
Часть 5: Что дальше с мобильной разработкой
Слушайте наш подкаст в iTunes и SoundCloud, читайте нас на Medium, контрибьютьте на GitHub, общайтесь в группе Telegram, следите в Twitter и канале Telegram, рекомендуйте в VK и Facebook.