|
| 1 | +# Responsive Layout in React Native |
| 2 | + |
| 3 | +> The challenges encountered when porting a classic React application to React Native are discussed in this article. It focuses on supporting various device form factors, including the Galaxy Fold. |
| 4 | +
|
| 5 | +## Проблема |
| 6 | + |
| 7 | +Some time ago, I started developing a mobile application with React Native. The issue is that the application needs to support both the Galaxy Fold and Samsung DeX. |
| 8 | + |
| 9 | + |
| 10 | + |
| 11 | +As a result, the question of implementing adaptive layout forms arose. The same application needs to render forms in a single column mode on phones and two columns in tablet mode. If different layout codes are written, it would require duplicating form logic; at the very least, a new field would be added twice. |
| 12 | + |
| 13 | + |
| 14 | +Yoga Layout, the layout engine from React Native, only supports flexbox. Consequently, implementing layouts correctly while avoiding excessive nesting of Views is a non-trivial task: nested groups must be displayed from the top edge of the parent, and input fields must align to the bottom edge of the display line (baseline). |
| 15 | + |
| 16 | + |
| 17 | + |
| 18 | +Ignoring the rule of binding field groups to the top edge results in an ugly layout. |
| 19 | + |
| 20 | + |
| 21 | + |
| 22 | +Fields within a group should be bound to the bottom edge of the row. |
| 23 | + |
| 24 | + |
| 25 | + |
| 26 | +The ugliness of having a top baseline for fields is not immediately obvious but becomes very noticeable when using standard fields from Material 1 on Android 4. |
| 27 | + |
| 28 | + |
| 29 | + |
| 30 | +## Solution |
| 31 | + |
| 32 | +To delegate the complex task of proper form layout to a less specialized developer, a templating system was developed that generates layouts according to the rules mentioned above from a JSON template. An example template is shown in the code block below: |
| 33 | + |
| 34 | +```tsx |
| 35 | +import { One, FieldType, TypedField } from 'rn-declarative'; |
| 36 | + |
| 37 | +import { Text } from '@ui-kitten/components'; |
| 38 | +import { ScrollView } from 'react-native'; |
| 39 | + |
| 40 | +const fields: TypedField[] = [ |
| 41 | + { |
| 42 | + type: FieldType.Component, |
| 43 | + style: { |
| 44 | + justifyContent: 'center', |
| 45 | + width: '100%', |
| 46 | + height: 125, |
| 47 | + }, |
| 48 | + element: () => ( |
| 49 | + <Text category='h4'> |
| 50 | + Adaptive columns |
| 51 | + </Text> |
| 52 | + ), |
| 53 | + }, |
| 54 | + { |
| 55 | + type: FieldType.Group, |
| 56 | + style: { |
| 57 | + width: '100%', |
| 58 | + }, |
| 59 | + fields: [ |
| 60 | + { |
| 61 | + type: FieldType.Group, |
| 62 | + phoneStyle: { |
| 63 | + width: '100%', |
| 64 | + }, |
| 65 | + tabletStyle: { |
| 66 | + width: '50%', |
| 67 | + }, |
| 68 | + desktopStyle: { |
| 69 | + width: '25%', |
| 70 | + }, |
| 71 | + fields: [ |
| 72 | + { |
| 73 | + type: FieldType.Component, |
| 74 | + style: { |
| 75 | + width: '100%', |
| 76 | + }, |
| 77 | + element: () => ( |
| 78 | + <Text category='h6'> |
| 79 | + FieldType.Text |
| 80 | + </Text> |
| 81 | + ), |
| 82 | + }, |
| 83 | + { |
| 84 | + type: FieldType.Text, |
| 85 | + style: { |
| 86 | + width: '100%', |
| 87 | + }, |
| 88 | + name: 'text', |
| 89 | + title: 'Text', |
| 90 | + description: 'Single line', |
| 91 | + }, |
| 92 | + { |
| 93 | + type: FieldType.Text, |
| 94 | + style: { |
| 95 | + width: '100%', |
| 96 | + }, |
| 97 | + validation: { |
| 98 | + required: true, |
| 99 | + }, |
| 100 | + dirty: true, |
| 101 | + name: 'text_invalid', |
| 102 | + title: 'Text', |
| 103 | + description: 'Invalid', |
| 104 | + }, |
| 105 | + { |
| 106 | + type: FieldType.Text, |
| 107 | + style: { |
| 108 | + width: '100%', |
| 109 | + }, |
| 110 | + inputMultiline: true, |
| 111 | + name: 'text', |
| 112 | + title: 'Text', |
| 113 | + description: 'Multi line', |
| 114 | + }, |
| 115 | + ], |
| 116 | + }, |
| 117 | + |
| 118 | + ... |
| 119 | + |
| 120 | +]; |
| 121 | + |
| 122 | +export const MainPage = () => { |
| 123 | + return ( |
| 124 | + <ScrollView> |
| 125 | + <One fields={fields} onChange={console.log} /> |
| 126 | + </ScrollView> |
| 127 | + ); |
| 128 | +}; |
| 129 | + |
| 130 | +export default MainPage; |
| 131 | +``` |
| 132 | +
|
| 133 | +The library is divided into two modules: [rn-declarative](https://www.npmjs.com/package/rn-declarative) and [rn-declarative-eva](https://www.npmjs.com/package/rn-declarative-eva) . The first contains the core logic and does not depend on a UI kit: it can be installed in any project regardless of the `react-native` version or framework (both `Expo` and `react-native-community` starter kits are supported). Besides `react` and `react-native`, there are no other dependencies. |
| 134 | +
|
| 135 | +```tsx |
| 136 | +import { useMediaContext } from 'rn-declarative' |
| 137 | + |
| 138 | +... |
| 139 | + |
| 140 | +const { isPhone, isTablet, isDesktop } = useMediaContext(); |
| 141 | +``` |
| 142 | +
|
| 143 | +Layout and field widths are configured using `phoneStyle`, `tabletStyle`, and `desktopStyle` properties. If you don't want to change the style based on the device form factor, you can just use `style`. Connecting a UI Kit is done through the context with slots `<OneSlotFactory />` for implementing `FieldType`. |
| 144 | +
|
| 145 | +```tsx |
| 146 | +import { Toggle } from '@ui-kitten/components'; |
| 147 | +import { OneSlotFactory, ISwitchSlot } from 'rn-declarative'; |
| 148 | + |
| 149 | +export const Switch = ({ |
| 150 | + disabled, |
| 151 | + value, |
| 152 | + onChange, |
| 153 | + onFocus, |
| 154 | + onBlur, |
| 155 | + title, |
| 156 | +}: ISwitchSlot) => { |
| 157 | + return ( |
| 158 | + <Toggle |
| 159 | + checked={Boolean(value)} |
| 160 | + disabled={disabled} |
| 161 | + onChange={() => onChange(!value)} |
| 162 | + onFocus={onFocus} |
| 163 | + onBlur={onBlur} |
| 164 | + > |
| 165 | + {title} |
| 166 | + </Toggle> |
| 167 | + ); |
| 168 | +}; |
| 169 | + |
| 170 | +... |
| 171 | + |
| 172 | +const defaultSlots = { |
| 173 | + CheckBox, |
| 174 | + Combo, |
| 175 | + Items, |
| 176 | + Radio, |
| 177 | + Button, |
| 178 | + Text, |
| 179 | + Switch, |
| 180 | + YesNo, |
| 181 | +}; |
| 182 | + |
| 183 | +... |
| 184 | + |
| 185 | +<OneSlotFactory |
| 186 | + {...defaultSlots} |
| 187 | +> |
| 188 | + {children} |
| 189 | +</OneSlotFactory> |
| 190 | +``` |
| 191 | +
|
| 192 | +P.S. Any other component or custom layout can be seamlessly integrated through `FieldType.Component` (with `onChange` and `value`) or `FieldType.Layout`. |
| 193 | +
|
| 194 | +```tsx |
| 195 | +{ |
| 196 | + type: FieldType.Component, |
| 197 | + element: () => ( |
| 198 | + <Text category='h4'> |
| 199 | + Sample component |
| 200 | + </Text> |
| 201 | + ), |
| 202 | +}, |
| 203 | + |
| 204 | +... |
| 205 | + |
| 206 | +{ |
| 207 | + type: FieldType.Layout, |
| 208 | + customLayout: ({ children }) => ( |
| 209 | + <ScrollView> |
| 210 | + {children} |
| 211 | + </ScrollView> |
| 212 | + ), |
| 213 | +}, |
| 214 | +``` |
| 215 | +
|
| 216 | +The component code is published on GitHub and can be viewed at: |
| 217 | +[https://github.com/react-declarative/rn-declarative/](https://github.com/react-declarative/rn-declarative/) |
| 218 | +
|
| 219 | +Thank you for your attention! |
0 commit comments