- PVSM.RU - https://www.pvsm.ru -

Kivy. Xamarin. React Native. Три фреймворка — один эксперимент (часть 3)

Kivy. Xamarin. React Native. Три фреймворка — один эксперимент (часть 3) - 1
Задача сравнения фреймворков очень неблагодарное занятие, предпочтения у разработчиков разные, технологии меняются очень быстро. Слишком быстро. Эта статья, устареет еще до того момента как я нажму кнопочку “опубликовать“.

Попытки сравнить были, так, порядка пяти лет назад, ребята (Colin Eberhardt и Chris Price) воодушевили ряд разработчиков сделать приложение для поиска недвижимости по четко составленному ТЗ. Идея классная, мы даже участвовали и сделали версию этого приложения на DevExtreme [1]. Но в плане поддержки такой проект это ад и сейчас проект Property Cross [2], представляет некоторый исторический пласт, который вызывает ностальгию и теплые чувства, но вряд ли несет практическую пользу.

Если брать только js мир, то есть довольно живой проект todomvc [3], который сравнивает только js часть, без упаковки в мобильное, десктопное или какое бы то ни было приложение. Проект живой и поддерживается. Скорее всего, есть еще очень классные примеры, которые мы не заметили в выдаче гугла когда готовили статью, но не будем огорчаться из-за этого.

Наш эксперимент менее амбициозен и показывает только текущий срез технологий, даже его малую часть. Вот ссылочка на первую [4] и вторую [5] статьи эксперимента.

Дальнейшее чтиво это третья статья, о том как сделать приложение на React Native по ТЗ. Оно очень похоже на сотни, а может быть и тысячи, других пересказов документации о том как сделать приложение на React Native. Дорогой читатель, я тебя предупредил, совесть моя чиста.

Вспоминаем ТЗ

Вообще, его можно полностью посмотреть в первой статье. Но я добавлю картинку того, что должно получится в итоге. Картинок мало не бывает.
image

Щепотка матчасти. Что такое React Native

React Native — фреймворк для создания кроссплатформенных мобильных приложений от Facebook. Как и в «обычном» React для веб, UI приложения собирается из кирпичиков — компонентов, которые реагируют на изменение своего состояния (state) и свойств им переданных (props), но, в отличие от веб, рендерятся в нативные контролы.

В идеале, используются принципы иммутабельности и чистые фунции, что обеспечивает простоту и изолированность тестирования. И тут стоит заметить, что сам по себе React очень простой, и эта простота переходит и в мобильную часть.

Дополнительные надстройки в нативном и JS коде сглаживают различие между платформами, когда это возможно. Фактически React Native обеспечивает некоторую унификацию свойств для компонента в каждой операционной системе.

Например, ScrollView, и HorizontalScrollView это 2 разных компонента в Android. А в iOS UIScrollView, который поддерживает как горизонтальный так и вертикальный скролл. А в React Native мы будем использовать следующий кроссплатформенный код:

<ScrollView horizontal={true}/>

При грамотном подходе на выходе получаем «честное» нативное приложение, работающее на iOS и Android.

В идеальном мире, разрабатывая на React Native, вам не придется писать на Java или Objective-C. Но такая возможность есть, когда необходимо реализовать компонент, который выходит за рамки возможностей React Native.

С этим много играли разработчики из Airbnb, и мы можем посмотреть много достойных реализаций в реакт комьюнити, которые раньше находились в их репозитории. Например Lottie [6] — библиотека для импорта анимаций из Adobe After Effects, или кросс-платформенные карты [7].

JS код в приложении исполняется на движке JavaScriptCore [8]. Коммуникация между нативным кодом и JS осуществляется с помощью асинхронного моста (bridge), который позволяет передавать свойства (props), вызывать события (events) и выполнять коллбеки.
Kivy. Xamarin. React Native. Три фреймворка — один эксперимент (часть 3) - 3
Картинка взята из отличной переработки документации React Made Native Easy [9]. (Настоятельно рекомендую к прочтению.)

В процессе сборки для преобразования JS кода используется новомодный babel, это позволяет использовать новый синтаксис ES6, а также некоторые фичи ES8 (например async-await). Если вы, мой дорогой читатель, js разработчик, то понимаете как хорошо, когда есть спред оператор и как плохо, когда его нет.

Для верстки страниц используется технология flexbox, реализованная кроссплатформенным движком Yoga. Она имеет отличия от браузерного flexbox, но они незначительны и, в основном, касаются дефолтов. Конечно, есть нюансы, но вам обязательно повезет, и все будет только согласно документации.

Подготовка и развертывание стека. Ламповый терминал

Для работы с RN нам потребуются Node.js [10] и менеджер пакетов npm, который идет в комплекте. Не обязательно, но очень желательно установить на свой девайс приложение Expo [11]. Оно позволит запустить наш проект на телефоне, а также собрать и запустить приложение для iOS, когда у вас под рукой нет macOS.

Создадим новое приложение. Для этого используем пакет create-react-native-app [12].

В терминале выполняем:

npm install -g create-react-native-app
create-react-native-app notes
cd notes
npm run start

Сканируем QR-код с помощью Expo или вводим ссылку из терминала, или даже отсылаем ссылку себе на телефон, прямо из терминала.

У меня вообще есть подозрение, что в разработчики cli для react native затесался седоволосый старец, который застал roguelike игрушки без ui, когда есть только терминал, и вместо топовой видеокарты только твоя фантазия.
Kivy. Xamarin. React Native. Три фреймворка — один эксперимент (часть 3) - 4
Но мы, тем временем, только что создали и запустили “Hello World” приложение.

И целого “Hello World”-a мало. Анализируем ТЗ

Согласно ТЗ, структура данных приложения будет такой

Note: {
	userName: string,
	avatar: string,
	editTime: string,
	text: string
}
Project: { name: string, notes: Array<Note>  }
Projects: Array<Project>

Для работы с такими данными я бы взял какое-нибудь очень модное решение на основе CQRS. Это позволило бы сохранить целостность данных, обеспечить высокую скорость чтения с возможностью перестраивания проекций, а также быстрый деплой в облако одной командой. Как Resolve [13], который разрабатывают наши коллеги.

Но не возьму, у нас же простой эксперимент, без бекенда. И для простоты буду использовать архитектуру flux [14], в частности ее реализацию — redux [15]. Данные из состояния приложения приходят в компоненты в качестве props. Компоненты могут вызвать actions, чтобы обновить данные.

Приложение будет иметь 3 экрана, все согласно ТЗ:

  • список проектов — Projects,
  • детальная страница проекта со списком заметок — Project,
  • детальная страница заметки — Note

Для навигации между экранами буду использовать стандартную библиотеку react-navigation [16]. Циферки около графика на странице библиотеки, показывают сколько раз ее скачивают в неделю. Сейчас там порядка 100 тысяч, в неделю. Хорошо, что я не один выбрал такую библиотеку для навигации. И да, можно посмотреть циферки у других npm пакетов, которые я указал в этой статье, чтобы примерно понимать количество пользователей данной технологии на данный момент времени.

Создаем приложение

Для React Native компонент App из файла App.js это точка входа в приложение.

export default class App extends Component {
 render() {
   return (
     <Provider store={store}>
       <Navigator />
     </Provider>
   )
 }
}

Store с данными и состоянием приложения подключается компонентом Provider из библиотеки react-redux. Это обеспечивает проброс данных для вложенных компонентов.

Создадим навигатор для переходов между экранами в приложении. Он четко отражает структуру приложения, заявленную в эксперименте, и отрисовывает анимированные переходы между экранами для каждой из платформ.

const Navigator = createStackNavigator({
   Projects: {
     screen: Projects
   },
   Project: {
     screen: Project
   },
   Note: {
     screen: Note
   }
 })

Экраны навигатора это компоненты — контейнеры. Они получают данные из стейта приложения.

Список проектов — Projects

На экране со списком проектов будет список и кнопка добавления проекта — в хедере окна справа. Новый проект будем создавать на экране Project.

Для навигации используем объект navigation, который передал в props родительский компонент — навигатор.

export class Projects extends PureComponent {
 static navigationOptions = ({ navigation }) => ({
   headerRight: (
     <AddButton onPress={() => navigation.navigate('Project')} />
   )
 })

 navigateProject = project => {
   this.props.navigation.navigate('Project', {
     projectId: project.id,
     name: project.name
   })
 }

 render() {
   return (
     <ProjectList
       projects={this.props.projects}
       onPressProject={this.navigateProject}
     />
   )
 }
}

Для вывода списка проектов будем использовать FlatList — кросс-платформенный список с виртуализацией:

export class ProjectList extends PureComponent {
 static propTypes = {
   projects: ProjectsType,
   onPressProject: PropTypes.func
 }

 renderItem = ({ item }) => (
   <ProjectListItem
     project={item}
     onPressProject={this.props.onPressProject}
   />
 )

 render() {
   return (
     <FlatList
       data={this.props.projects}
       keyExtractor={item => item.id}
       renderItem={this.renderItem}
     />
   )
 }
}

Для каждого элемента задаем уникальный ключ — у нас это id элемента. Это нужно для того, чтобы реакт мог различать элементы в списке и обновлять только те, которые изменились.

Добавим компонент для элемента списка.

export class ProjectListItem extends PureComponent {
 static propTypes = {
   project: ProjectType,
   onPressProject: PropTypes.func
 }

 onPressProject = () => {
   const { project, onPressProject } = this.props
   onPressProject(project)
 }

 render() {
   return (
     <TouchableOpacity onPress={this.onPressProject}>
       <View style={styles.project}>
         <Text style={styles.name}>{this.props.project.name}</Text>
       </View>
     </TouchableOpacity>
   )
 }
}

TouchableOpactity — обертка, реагирующая на нажатия. При нажатии вложенный компонент становится прозрачнее.
View — аналог div для веб — базовый компонент разметки.
Text — контейнер для текста.
Добавим стили:

const styles = StyleSheet.create({
 project: {
   paddingVertical: 30,
   paddingHorizontal: 15,
   backgroundColor: 'white',
   borderBottomWidth: StyleSheet.hairlineWidth,
   borderColor: 'gray'
 },
 name: {
   fontSize: 16
 }
})

Синтаксис стилей напоминает css, главное отличие — стилизовать можно только сам компонент (например нельзя задать размер шрифта для всего приложения, только для конкретного компонента Text)
Kivy. Xamarin. React Native. Три фреймворка — один эксперимент (часть 3) - 5

Детальная страница проекта со списком заметок — Project

Аналогично создаем детальную страницу. Отличия только в наличии заголовка в навигаторе и дополнительного инпута. В навигаторе зададим заголовок — название проекта. Если id проекта не задан — предложим ввести название проекта и создадим новый.

export class Project extends PureComponent {
 static navigationOptions = ({ navigation }) => {
   const projectId = navigation.getParam('projectId')
   return {
     title: navigation.getParam('name', ''),
     headerRight: (
       <AddButton
         onPress={() => navigation.navigate('Note', { projectId })}
       />
     )
   }
 }

 removeNote = noteId => {
   const { projectId, removeNote } = this.props
   removeNote(projectId, noteId)
 }

 navigateNote = noteId => {
   const { projectId, navigation } = this.props
   navigation.navigate('Note', { noteId, projectId })
 }

 createProject = name => {
   const newProjectId = shortid.generate()
   this.props.navigation.setParams({ projectId: newProjectId, name })
   this.props.addProject(newProjectId, name)
 }

 render() {
   const { projectId, project } = this.props

   if (!projectId) {
     return (
       <ProjectNameInput
         onSubmitEditing={this.createProject}
       />
     )
   }

   return (
     <NoteList
       notes={project.notes}
       onNavigateNote={this.navigateNote}
       onRemoveNote={this.removeNote}
     />
   )
 }
}

Страница проекта представляет собой список заметок. По ТЗ для каждой заметки есть контекстное меню с редактированием и удалением. Также удалить заметку можно свайпом. В React Native существует отдельный список, с возможностью свайпа — SwipeableFlatList.

<SwipeableFlatList
 data={this.props.notes}
 bounceFirstRowOnMount={false}
 keyExtractor={item => item.id}
 maxSwipeDistance={MAX_SWIPE_DISTANCE}
 renderQuickActions={this.renderQuickActions}
 renderItem={this.renderItem}
/>

При удалении заметки мы запросим подтверждение, для этого вызовем стандартный системный Alert

onRemoveNote = noteId => {
 Alert.alert(
   'Remove Note',
   'Do you want to remove note ?',
   [
     { text: 'Cancel', onPress: () => {}},
     { text: 'Remove', onPress: () => this.props.onRemoveNote(noteId) }
   ]
 )
}

Kivy. Xamarin. React Native. Три фреймворка — один эксперимент (часть 3) - 6

Есть интересный момент для контекстного меню. В отличие от алерта, его реализация в RN для Android и iOS различается.

Для андроид используем попап меню

showPopupMenu = () => {
 const button = findNodeHandle(this._buttonRef)
 UIManager.showPopupMenu(
   button,
   [ 'Edit', 'Delete' ],
   e => console.error(e),
   (e, i) => this.onPressMenu(i)
 )
}

Для iOS — actionSheet

showActionSheet = () => {
 ActionSheetIOS.showActionSheetWithOptions({
     options: [ 'Edit', 'Delete', 'Cancel' ],
     destructiveButtonIndex: 1,
     cancelButtonIndex: 2
   },
   this.onPressMenu
 )
}

Есть несколько способов разделить платформо-зависимый код. Мы воспользуемся объектом Platform.

onOpenMenu = Platform.select({
 android: this.showPopupMenu,
 ios: this.showActionSheet
})

Kivy. Xamarin. React Native. Три фреймворка — один эксперимент (часть 3) - 7

Детальная страница заметки — Note

Страница заметки также довольно примитивна. Но, в отличие от предыдущих, мы используем state для хранения промежуточных результатов ввода пользователя.

export class Note extends PureComponent {
 static navigationOptions = ({ navigation }) => ({
   headerRight: (
     <SaveButton onPress={navigation.getParam('onSaveNote')} />
   )
 })

 state = {
   noteText: ''
 }

 componentDidMount() {
   this.props.navigation.setParams({ onSaveNote: this.onSaveNote })
 }

 onSaveNote = () => {
   Keyboard.dismiss()

   const { projectId, noteId, note, navigation, addNote, editNote } = this.props
   const { noteText } = this.state

   if (!noteId) {
     const newNoteId = shortId.generate()
     navigation.setParams({ noteId: newNoteId })
     addNote(projectId, newNoteId, noteText)
   } else if (noteText && noteText !== note.text) {
     editNote(projectId, noteId, noteText)
   }
 }

 onChangeNote = noteText => {
   this.setState({ noteText })
 }

 render() {
   const initialTextValue = this.props.note ?
     this.props.note.text : ''
   const noteText = this.state.noteText || initialTextValue

   return (
     <NoteDetail
       noteText={noteText}
       onChangeNoteText={this.onChangeNote}
     />
   )
 }
}

Детальный экран заметки — классический “глупый” компонент — докладывает наверх об изменении текста и показывает текст, который ему передает родитель

export class NoteDetail extends PureComponent {
 static propTypes = {
   noteText: PropTypes.string,
   onChangeNoteText: PropTypes.func
 }

 render() {
   const { noteText, onChangeNoteText } = this.props
   return (
     <View style={styles.note}>
       <TextInput
         multiline
         style={styles.noteText}
         value={noteText}
         placeholder="Type note text here ..."
         underlineColorAndroid="transparent"
         onChangeText={onChangeNoteText}
       />
     </View>
   )
 }
}

Kivy. Xamarin. React Native. Три фреймворка — один эксперимент (часть 3) - 8

Итого мы получили приложение как в ТЗ. Эксперимент завершен. Код приложения можно посмотреть в общем репозитории [17]

Итого, плюсы и минусы React Native

Плюсы:

React Native привычен и понятен разработчикам, знакомым с React и инфраструктурой Node.js и npm. Есть возможность использовать все подходы и библиотеки, что и для обычного React.

Огромное количество js пакетов из npm. Скорее всего, большая часть стандартных задач уже решена и возможно под MIT лицензией.

Большое комьюнити. Как индивидуальные разработчики так и крупные компании использовали RN для разработки, и продолжают использовать.

Много готовых наборов UI компонентов, таких как NativeBase [18], React Native Elements [19], библиотеки от крупных компаний типа Facebook, Airbnb, Wix.com.

Понятный инструментарий, обеспечивающий удобную разработку приложения от Hello World до Instagram [20].

Минусы:

Приложение стартует медленнее нативного и есть некоторые сложности дебага. JS код в дебаггере и без него работает на разных движках. Об этой проблеме очень хорошо написали Airbnb в серии статей [21], почему они отказались от React Native в разработке.

Так как инструментарий состоит из множества пакетов, которые разрабатываются отдельно, существует вероятность конфликта версий и разлома.

Не все можно сделать без нативного кода. И когда вносишь изменения в нативный код, то теряешь возможность использовать Expo и вынуждаешь себя собирать приложение стандартными средствами нативной разработки.

Большое спасибо Mirimon [22] и HeaTTheatR [23] за приглашение поучаствовать в этом эксперименте. Было увлекательно. На последок добавлю голосовалку.

Автор: SeOd

Источник [24]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/290722

Ссылки в тексте:

[1] DevExtreme: https://js.devexpress.com/

[2] Property Cross: http://propertycross.com/

[3] todomvc: http://todomvc.com/

[4] первую: https://habr.com/post/420691/

[5] вторую: https://habr.com/company/devexpress/blog/420999/

[6] Lottie: https://github.com/react-community/lottie-react-native

[7] кросс-платформенные карты: https://github.com/react-community/react-native-maps

[8] JavaScriptCore: https://trac.webkit.org/wiki/JavaScriptCore

[9] React Made Native Easy: https://www.reactnative.guide/index.html

[10] Node.js: https://nodejs.org

[11] Expo: https://expo.io/

[12] create-react-native-app: https://github.com/react-community/create-react-native-app

[13] Resolve: https://github.com/reimagined/resolve

[14] flux: https://facebook.github.io/flux/

[15] redux: https://redux.js.org/

[16] react-navigation: https://www.npmjs.com/package/react-navigation

[17] репозитории: https://github.com/Mirimon/KivyXamarinReactComparison/tree/master/ReactNative

[18] NativeBase: https://github.com/GeekyAnts/NativeBase

[19] React Native Elements: https://github.com/react-native-training/react-native-elements

[20] Instagram: https://instagram-engineering.com/react-native-at-instagram-dd828a9a90c7

[21] серии статей: https://medium.com/airbnb-engineering/sunsetting-react-native-1868ba28e30a

[22] Mirimon: https://habr.com/users/mirimon/

[23] HeaTTheatR: https://habr.com/users/heattheatr/

[24] Источник: https://habr.com/post/421571/?utm_campaign=421571