Проблема
Некоторое время назад начал разработку мобильного приложения на React Native. Проблема в том, что требованием к приложению является Galaxy Fold и Samsung DeX
![Мечта верстальщика Мечта верстальщика](https://habrastorage.org/getpro/habr/upload_files/b14/53a/f01/b1453af0179cb259dc6385f9a7a744e8.png)
Как следствие, встал вопрос реализации адаптивной верстки форм.Одно и тоже приложение должно рисовать формы в одну колонку в режиме телефона и две колонки в режиме планшета. Если писать разный код компоновок, придется дублировать логику форм, как минимум, новое поле добавляется два раза.
![Android Large Desktop - эмулятор Android с динамическим размером окон Android Large Desktop - эмулятор Android с динамическим размером окон](https://habrastorage.org/getpro/habr/upload_files/8c2/80e/261/8c280e2613e0162ae36f2e54334b7506.png)
Yoga Layout, движок верстки из React Native, поддерживает только flexbox. В результате, грамотно реализовать верстку, избегая излишней вложенности View, является нетривиальной задачей: вложенные группы должны отображаться от верхнего края родителя, поля ввода должны равняться на нижний край строки отображения (baseline)
Правила компоновки
![Группы полей отмечены синим Группы полей отмечены синим](https://habrastorage.org/getpro/habr/upload_files/103/86e/58f/10386e58f9477a36e6679e31767361d3.png)
Если принебречь правилом привязки групп полей к верхнему краю, получится уродство
![Технически запрограммировано без ошибок, визуально выглядит как ошибка Технически запрограммировано без ошибок, визуально выглядит как ошибка](https://habrastorage.org/getpro/habr/upload_files/843/726/3a9/8437263a943dd7db03ec83c88a630892.png)
Поля внутри группы должны быть привязаны к нижнему краю строки
![Красным - родитель и baseline, синим - потомки Красным - родитель и baseline, синим - потомки](https://habrastorage.org/getpro/habr/upload_files/1a5/3e9/b8d/1a53e9b8de98b0725bf97faf8eeec24d.png)
Уродство при верхнем baseline для полей не очевидно с первого взгляда, но очень бросается при использование standard полей из Material 1 на Android 4
![Верхний baseline для полей с chip и нижним отчерком - уродство Верхний baseline для полей с chip и нижним отчерком - уродство](https://habrastorage.org/getpro/habr/upload_files/3c2/300/91a/3c230091a4e9597ed694b4e210780af3.png)
Решение проблемы
Для того, чтобы делегировать сложную задачу грамотной компоновки форм более дешевому специалисту, был разработан шаблонизатор, генерирующий верстку по указанным выше правилам из JSON шаблона. Пример шаблона в блоке кода ниже
import { One, FieldType, TypedField } from 'rn-declarative';
import { Text } from '@ui-kitten/components';
import { ScrollView } from 'react-native';
const fields: TypedField[] = [
{
type: FieldType.Component,
style: {
justifyContent: 'center',
width: '100%',
height: 125,
},
element: () => (
<Text category='h4'>
Adaptive columns
</Text>
),
},
{
type: FieldType.Group,
style: {
width: '100%',
},
fields: [
{
type: FieldType.Group,
phoneStyle: {
width: '100%',
},
tabletStyle: {
width: '50%',
},
desktopStyle: {
width: '25%',
},
fields: [
{
type: FieldType.Component,
style: {
width: '100%',
},
element: () => (
<Text category='h6'>
FieldType.Text
</Text>
),
},
{
type: FieldType.Text,
style: {
width: '100%',
},
name: 'text',
title: 'Text',
description: 'Single line',
},
{
type: FieldType.Text,
style: {
width: '100%',
},
validation: {
required: true,
},
dirty: true,
name: 'text_invalid',
title: 'Text',
description: 'Invalid',
},
{
type: FieldType.Text,
style: {
width: '100%',
},
inputMultiline: true,
name: 'text',
title: 'Text',
description: 'Multi line',
},
],
},
...
];
export const MainPage = () => {
return (
<ScrollView>
<One fields={fields} onChange={console.log} />
</ScrollView>
);
};
export default MainPage;
Библиотека разделена на два модуля: rn-declarative и rn-declarative-eva. Первая содержит базовую логику и не зависит от UI kit: может быть установлена в любом проекте не зависимо от версии react-native или фреймворка (поддерживается как Expo, так и starter kit от react-native-community). Кроме react и react-native других зависимостей нет.
import { useMediaContext } from 'rn-declarative'
...
const { isPhone, isTablet, isDesktop } = useMediaContext();
Настройка ширины компоновок и полей осуществляется через свойства объектов phoneStyle, tabletStyle и desktopStyle. Если не хотим менять стиль исходя из форм-фактора устройства, можно написать просто style. Подключение UI Kit осуществляется через контекст с слотами <OneSlotFactory /> для подключения реализации FieldType.
import { Toggle } from '@ui-kitten/components';
import { OneSlotFactory, ISwitchSlot } from 'rn-declarative';
export const Switch = ({
disabled,
value,
onChange,
onFocus,
onBlur,
title,
}: ISwitchSlot) => {
return (
<Toggle
checked={Boolean(value)}
disabled={disabled}
onChange={() => onChange(!value)}
onFocus={onFocus}
onBlur={onBlur}
>
{title}
</Toggle>
);
};
...
const defaultSlots = {
CheckBox,
Combo,
Items,
Radio,
Button,
Text,
Switch,
YesNo,
};
...
<OneSlotFactory
{...defaultSlots}
>
{children}
</OneSlotFactory>
Код компонента опубликован на Github, посмотреть можно по ссылке.
Спасибо за внимание!