Хабрахабр

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


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

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

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

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

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

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

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

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

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

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

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

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

<ScrollView horizontal=/>

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

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

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

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

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

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

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

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

Для работы с 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. Но не возьму, у нас же простой эксперимент, без бекенда. Компоненты могут вызвать actions, чтобы обновить данные. Данные из состояния приложения приходят в компоненты в качестве props.

Приложение будет иметь 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.

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

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

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

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

Минусы:

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

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

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

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть