Проблема

Некоторое время назад начал разработку мобильного приложения на React Native. Проблема в том, что требованием к приложению является Galaxy Fold и Samsung DeX

Мечта верстальщика
Мечта верстальщика

Как следствие, встал вопрос реализации адаптивной верстки форм.Одно и тоже приложение должно рисовать формы в одну колонку в режиме телефона и две колонки в режиме планшета. Если писать разный код компоновок, придется дублировать логику форм, как минимум, новое поле добавляется два раза.

Android Large Desktop - эмулятор Android с динамическим размером окон
Android Large Desktop - эмулятор Android с динамическим размером окон

Yoga Layout, движок верстки из React Native, поддерживает только flexbox. В результате, грамотно реализовать верстку, избегая излишней вложенности View, является нетривиальной задачей: вложенные группы должны отображаться от верхнего края родителя, поля ввода должны равняться на нижний край строки отображения (baseline)

Правила компоновки

Группы полей отмечены синим
Группы полей отмечены синим

Если принебречь правилом привязки групп полей к верхнему краю, получится уродство

Технически запрограммировано без ошибок, визуально выглядит как ошибка
Технически запрограммировано без ошибок, визуально выглядит как ошибка

Поля внутри группы должны быть привязаны к нижнему краю строки

Красным - родитель и baseline, синим - потомки
Красным - родитель и baseline, синим - потомки

Уродство при верхнем baseline для полей не очевидно с первого взгляда, но очень бросается при использование standard полей из Material 1 на Android 4

Верхний baseline для полей с chip и нижним отчерком - уродство
Верхний baseline для полей с chip и нижним отчерком - уродство

Решение проблемы

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

Спасибо за внимание!

Комментарии (0)