Pull to refresh
0

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

Reading time 10 min
Views 10K

Задача сравнения фреймворков очень неблагодарное занятие, предпочтения у разработчиков разные, технологии меняются очень быстро. Слишком быстро. Эта статья, устареет еще до того момента как я нажму кнопочку “опубликовать“.


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

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

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

Дальнейшее чтиво это третья статья, о том как сделать приложение на 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 — библиотека для импорта анимаций из Adobe After Effects, или кросс-платформенные карты.

JS код в приложении исполняется на движке JavaScriptCore. Коммуникация между нативным кодом и JS осуществляется с помощью асинхронного моста (bridge), который позволяет передавать свойства (props), вызывать события (events) и выполнять коллбеки.

Картинка взята из отличной переработки документации React Made Native Easy. (Настоятельно рекомендую к прочтению.)

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

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

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


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

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

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

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

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

У меня вообще есть подозрение, что в разработчики cli для react native затесался седоволосый старец, который застал roguelike игрушки без ui, когда есть только терминал, и вместо топовой видеокарты только твоя фантазия.

Но мы, тем временем, только что создали и запустили “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, который разрабатывают наши коллеги.

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

Приложение будет иметь 3 экрана, все согласно ТЗ:
  • список проектов — Projects,
  • детальная страница проекта со списком заметок — Project,
  • детальная страница заметки — Note


Для навигации между экранами буду использовать стандартную библиотеку react-navigation. Циферки около графика на странице библиотеки, показывают сколько раз ее скачивают в неделю. Сейчас там порядка 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)


Детальная страница проекта со списком заметок — 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) }
   ]
 )
}



Есть интересный момент для контекстного меню. В отличие от алерта, его реализация в 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
})



Детальная страница заметки — 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>
   )
 }
}



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

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


Плюсы:


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

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

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

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

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

Минусы:


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

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

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

Большое спасибо Mirimon и HeaTTheatR за приглашение поучаствовать в этом эксперименте. Было увлекательно. На последок добавлю голосовалку.
Only registered users can participate in poll. Log in, please.
Какой фреймворк вы используете для разработки мобильных приложений?
9.95% Kivy 20
27.86% Xamarin 56
31.84% React Native 64
6.97% Cordova\PhoneGap 14
1% Native Script 2
0.5% AIR 1
26.87% Пишу нативно 54
9.95% Другое 20
201 users voted. 116 users abstained.
Tags:
Hubs:
+17
Comments 20
Comments Comments 20

Articles

Information

Website
www.developersoft.ru
Registered
Founded
1998
Employees
201–500 employees
Location
Россия