الآن وقد انتهينا من إعداد بيئة التطوير، سنتعرف على أساسيات React Native وسنبدأ بتطوير تطبيقنا. سنتعلم في هذا الفصل كيف سنبني واجهة مستخدم بالاستعانة بمكوّنات React Native البنيوية، وكيف سننسق مظهر هذه المكونات البنيوية، وكيف سننتقل من واجهة عرض إلى أخرى، كما سنتعلم كيف سندير حالة النماذج بفعالية.
المكونات البنيوية
تعلمنا في الأقسام السابقة طريقة استخدام React في تعريف المكوّنات كدوال تتلقى الخصائص كوسطاء وتعيد شجرة من عناصر React. تُمثَّل هذه الشجرة عادةً باستخدام شيفرة JSX. كما رأينا كيفية استخدام المكتبة ReactDOM في بيئة المتصفح من أجل تحويل المكوّنات إلى شجرة DOM قابلة للتصيير ضمن المتصفح. لاحظ الشيفرة التالية التي تمثل مكوّنًا بسيطًا:
import React from 'react'; const HelloWorld = props => { return <div>Hello world!</div>; };
يعيد المكوّن HelloWorld
عنصر <div>
أُنشئ باستخدام عبارة JSX. وتذكر أن عبارة JSX ستُصرّف إلى استدعاءات للتابع React.createElement
كالتالي:
React.createElement('div', null, 'Hello world!');
سيُنشئ سطر الشيفرة السابق عنصر <div>
دون أية خصائص props وله عنصر ابن وحيد هو النص "Hello World". وعندما يُصيَّر هذا المكوّن إلى عنصر DOM جذري بتطبيق التابع ReactDOM.render
، سيُصيَّر العنصر <div>
إلى عنصر DOM المقابل.
وهكذا نرى أن React لا ترتبط ببيئة محددة كبيئة المتصفح، لكن وبالاستعانة بمكتبات مثل ReactDOM ستتمكن من تصيير مجموعة من المكوّنات المعرّفة مسبقًا كعناصر DOM في بيئة محددة. ندعو هذه المكوّنات المعرّفة مسبقًا في React Native بالمكوّنات البنيوية core components.
فالمكوّنات البنيوية هي مجموعة مكوّنات تؤمنها React Native قادرة على استخدام المكونات الأصلية لمنصة خلف الستار. لننفذ المثال السابق باستخدام React Native:
import React from 'react'; import { Text } from 'react-native'; const HelloWorld = props => { return <Text>Hello world!</Text>;};
لاحظ كيف أدرجنا المكوّن Text
من React Native، واستبدلنا العنصر<div>
بالعنصر <text>
. ستجد الكثير من عناصر DOM المقابلة لعناصر React Native، وسنستعرض بعض الأمثلة التي استقيناها من توثيق المكونات البنيوية:
-
المكوّن Text: ويعتبر المكون الوحيد من مكونات React Native والذي يمتلك أبناء على شكل قيم نصية، ويشابه العناصر
<h1>
و<strong>
. -
المكوّن View: ويمثل وحدات البناء الرئيسية لواجهة المستخدم، ويشابه العنصر
<div>
. -
المكوّن TextInput: وهو مكوّن لحقل نصي، ويشابه العنصر
<input>
. -
المكوّن TouchableWithoutFeedback: وغيره من المكونات القابلة للمس Touchable ويستخدم لالتقاط مختلف حوادث الضغط والنقر، ويشابه العنصر
<button>
.
هنالك بعض الاختلافات الواضحة بين المكوّنات البنيوية وعناصر DOM. يظهر الاختلاف الأول بأن المكوّن Text
هو المكوّن الوحيد الذي يمتلك أبناء على شكل قيم نصية في React Native. وبمعنًى آخر، لن تستطيع استبدال هذا المكوّن بالمكوّن View
مثلًا في المثال الذي أوردناه سابقًا.
يتعلق الاختلاف الثاني بمعالجات الأحداث. فقد اعتدنا عند العمل مع عناصر DOM على إضافة معالج أحداث مثل onClick
إلى أية عناصر مثل <div>
و<btton>
. لكن عليك قراءة توثيق الواجهة البرمجية في React Native لمعرفة أية معالجات وأية خصائص أخرى سيقبلها المكوّن الذي تستخدمه. إذ تؤمن -على سبيل المثال- عائلة مكوّنات اللمس Touchable components القدرة على التقاط الحركات التي تُرسم على الشاشة، كما يمكنها إظهار استجابات محددة عند تمييز هذه الحركات. ومن هذه المكوّنات أيضًا TouchableWithoutFeedback الذي يقبل الخاصية onPress
:
import React from 'react'; import { Text, TouchableWithoutFeedback, Alert } from 'react-native'; const TouchableText = props => { return ( <TouchableWithoutFeedback onPress={() => Alert.alert('You pressed the text!')} > <Text>You can press me</Text> </TouchableWithoutFeedback> ); };
سنبدأ الآن في هيكلة مشروعنا بعد أن اطلعنا على أساسيات المكونات البنيوية. لننشئ مجلدًا باسم "src" في المجلد الجذري للمشروع، ولننشئ ضمنه مجلدًا آخر باسم "components". أنشئ ملفًا باسم "Main.js" وضعه في المجلد "components"، على أن يحتوي هذا الملف الشيفرة التالية:
import React from 'react'; import Constants from 'expo-constants'; import { Text, StyleSheet, View } from 'react-native'; const styles = StyleSheet.create({ container: { marginTop: Constants.statusBarHeight, flexGrow: 1, flexShrink: 1, }, }); const Main = () => { return ( <View style={styles.container}> <Text>Rate Repository Application</Text> </View> ); }; export default Main;
لنستخدم الآن المكوّن Main
ضمن المكوّن App
الموجود في الملف "App.js" والذي يقع بدوره في المجلد الجذري للمشروع. استبدل محتويات الملف السابق بالشيفرة التالية:
import React from 'react'; import Main from './src/components/Main'; const App = () => { return <Main />; }; export default App;
إعادة تحميل التطبيق يدويا
رأينا كيف يعيد Expo تحميل التطبيق تلقائيًا عندما نجري تغييرات على الشيفرة. لكن قد تفشل عملية إعادة التحميل تلقائيًا في حالات معينة، وسنضطر إلى إعادة تحميل التطبيق يدويًا. تُنجز هذه العملية من خلال قائمة المطوّر التي يمكن الوصول إليها بهزّ جهازك أو باختيار الرابط "Shake Gesture" ضمن قائمة العتاد الصلب في محاكي iOS. كما يمكن أن تستخدم أيضًا الاختصار "D⌘" عندما يعمل تطبيقك على محاكي iOS، أو الاختصار "M⌘" عندما يعمل التطبيق على مقلّد Android في نظام macOS، والاختصار "Ctrl+M" لمقلد Android في نظامي Linux وWindows.
اضغط على "Reload" عندما تفتح قائمة المطوّر لإعادة تحميل التطبيق. لن تحتاج بعد ذلك إلى إعادة تحميل التطبيق، بل ستحدث العملية تلقائيًا.
التمرين 10.3
قائمة المستودعات المقيمة
سننجز في هذه التمارين النسخة الأولى من تطبيق قائمة المستودعات المُقيَّمة. ينبغي أن تتضمن القائمة الاسم الكامل للمستودع ووصفه ولغته وعدد التشعبات وعدد النجمات ومعدل التقييم وعدد التقييمات. ولحسن الحظ، تؤمن مكوّنًا مفيدًا لعرض قائمة من البيانات وهو FlatList.
انجز المكوّنين RepositoryList
وRepositoryItem
في الملفين "RepositoryList.jsx" و"RepositoryItem.jsx" وضعهما في المجلد "componenets". ينبغي أن يصيّر المكوّن RepositoryList
المكوّن البنيوي FlatList وأن يصيّر المكوّن RepositoryItem
عنصرًا واحدًا من القائمة
اقتباستلميح: استخدم الخاصية العائدة للمكون FlatList.
استخدم ذلك كأساس للملف "RepositoryItem.jsx":
import React from 'react'; import { FlatList, View, StyleSheet } from 'react-native'; const styles = StyleSheet.create({ separator: { height: 10, }, }); const repositories = [ { id: 'jaredpalmer.formik', fullName: 'jaredpalmer/formik', description: 'Build forms in React, without the tears', language: 'TypeScript', forksCount: 1589, stargazersCount: 21553, ratingAverage: 88, reviewCount: 4, ownerAvatarUrl: 'https://avatars2.githubusercontent.com/u/4060187?v=4', }, { id: 'rails.rails', fullName: 'rails/rails', description: 'Ruby on Rails', language: 'Ruby', forksCount: 18349, stargazersCount: 45377, ratingAverage: 100, reviewCount: 2, ownerAvatarUrl: 'https://avatars1.githubusercontent.com/u/4223?v=4', }, { id: 'django.django', fullName: 'django/django', description: 'The Web framework for perfectionists with deadlines.', language: 'Python', forksCount: 21015, stargazersCount: 48496, ratingAverage: 73, reviewCount: 5, ownerAvatarUrl: 'https://avatars2.githubusercontent.com/u/27804?v=4', }, { id: 'reduxjs.redux', fullName: 'reduxjs/redux', description: 'Predictable state container for JavaScript apps', language: 'TypeScript', forksCount: 13902, stargazersCount: 52869, ratingAverage: 0, reviewCount: 0, ownerAvatarUrl: 'https://avatars3.githubusercontent.com/u/13142323?v=4', }, ]; const ItemSeparator = () => <View style={styles.separator} />; const RepositoryList = () => { return ( <FlatList data={repositories} ItemSeparatorComponent={ItemSeparator} // other props /> ); }; export default RepositoryList;
لا تبدّل محتويات المتغيّر repositories
، فمن المفترض أن يحتوي على كل ما تحتاج إليه لإكمال التمرين. صيّر المكوّن ReositoryList
ضمن المكوّن Main
الذي أضفناه سابقّا إلى الملف "Main.jsx". ستبدو قائمة الكتب المُقَّيمة مشابهةً للتالي:
تنسيق التطبيق
بعد أن تعرفنا على آلية عمل المكوّنات، وكيفية استخدامها في بناء واجهة مستخدم بسيطة، لا بد من الالتفات إلى طرق تنسيق مظهر هذه التطبيقات. لقد رأينا في القسم 2 أنه من الممكن في بيئة المتصفح تعريف خصائص لتنسيق المكوّنات باستخدام CSS. كما رأينا أن هناك طريقتان لإنجاز ذلك: إما التنسيق ضمن السياق inline باستخدام الخاصية style
، أو عن طريق محددات التنسيق المناسبة selectors المكتوبة في ملف CSS منفصل.
ستجد تشابهًا واضحًا في الطريقة التي ترتبط بها خصائص التنسيق بمكونات React Native البنيوية والطريقة التي ترتبط بها هذه الخصائص بعناصر DOM. فمعظم مكوّنات React Native البنيوية تقبل الخاصية style
، وتقبل هذه الخاصية بدورها كائنًا يحمل خصائص تنسيقٍ مع قيمها. وتتطابق في أغلب الأحيان خصائص التنسيق هذه مع مقابلاتها في CSS، مع اختلاف بسيط وهو كتابة أسماء هذه الصفات بطريقة سنام الجمل camelCase. أي ستكتب خصائص مثل "padding-top" على الشكل "paddingTop". وسيوضح المثال التالي طريقة استخدام الخاصية style
:
import React from 'react'; import { Text, View } from 'react-native'; const BigBlueText = () => { return ( <View style={{ padding: 20 }}> <Text style={{ color: 'blue', fontSize: 24, fontWeight: '700' }}> Big blue text </Text> </View> ); };
بالإضافة إلى أسماء الخصائص، ستلاحظ اختلافًا آخر في المثال السابق. إذ ترتبط بخصائص CSS التي تقبل قيمًا عددية واحدات مثل px أو % أو em أو rem. بينما لا تمتلك الخصائص التي تتعلق بالأبعاد مثل width
وheight
وpadding
وكذلك font-size
في React Native أية واحدات. تمثل هذه القيم العددية بيكسلات مستقلة عن الكثافة density-independent pixels. ولو تساءلت عن خصائص التنسيق المتاحة لمكوّن بنيوي محدد، ستجد الجواب في أوراق التنسيق الزائفة Styling Cheat Sheet لإطار عمل React Native.
وبشكل عام، لا يعتبر تنسيق المكوًنات باستخدام الخاصية style
مباشرة فكرة جيدة، لأنها ستجعل شيفرة المكونات غير واضحة. ويفضّل بدلًا عن ذلك، تعريف التنسيق خارج دالة المكوّن باستخدام التابع StyleSheet.create. يقبل هذا التابع وسيطًا واحدًا على شكل كائن يتألف بحد ذاته من كائنات تنسيق مسماة ومحددة القيم، كما ينشئ مرجعًا للمكوّن البنيوي StyleSheet
من هذا الكائن. سنستعرض فيما يلي طريقة إعادة كتابة المثال السابق باستخدام التابع StyleSheet.create:
import React from 'react'; import { Text, View, StyleSheet } from 'react-native'; const styles = StyleSheet.create({ container: { padding: 20, }, text: { color: 'blue', fontSize: 24, fontWeight: '700', },}); const BigBlueText = () => { return ( <View style={styles.container}> <Text style={styles.text}> Big blue text <Text> </View> ); };
أنشأنا في الشيفرة السابقة كائني تنسيق مسمّيين هما: styles.container
وstyles.text
. يمكننا الوصول ضمن مكوّن إلى كائن تنسيق محدد بنفس الطريقة التي نصل بها إلى كائن صرف.
تقبل الخاصية style
مصفوفة من الكائنات بالإضافة إلى الكائنات المفردة. وتتالى كائنات التنسيق في المصفوفة من اليسار إلى اليمين، وبالتالي سيكون للتنسيقات التي تظهر لاحقًا أولوية التنفيذ. كما يمكن تطبيق ذلك بشكل عودي recursive، إذ يمكن على سبيل المثال، أن تحتوي مصفوفة على مصفوفة أخرى لكائنات التنسيق وهكذا. إن كانت إحدى قيم المصفوفة غير صالحة أو كانت "null" أو "undefined"، سيتم تجاهل هذه القيم. وهكذا يصبح من السهل تعريف تنسيقات شرطية كتلك التي تُبنى على أساس قيمة خاصية مثلًا، وإليكم مثالًا عن ذلك:
import React from 'react'; import { Text, View, StyleSheet } from 'react-native'; const styles = StyleSheet.create({ text: { color: 'grey', fontSize: 14, }, blueText: { color: 'blue', }, bigText: { fontSize: 24, fontWeight: '700', }, }); const FancyText = ({ isBlue, isBig, children }) => { const textStyles = [ styles.text, isBlue && styles.blueText, isBig && styles.bigText, ]; return <Text style={textStyles}>{children}</Text>; }; const Main = () => { return ( <> <FancyText>Simple text</FancyText> <FancyText isBlue>Blue text</FancyText> <FancyText isBig>Big text</FancyText> <FancyText isBig isBlue> Big blue text </FancyText> </> ); };
استخدمنا في الشيفرة السابقة العامل && في العبارة condition && exprIfTrue
. سيعطي تنفيذ هذه العبارة قيمة "experIfTrue" إن كانت قيمة "condition" هي "true"، وإلا سيعطي القيمة "condition" والتي ستأخذ في هذه الحالة القيمة "false". هذه الطريقة المختصرة شائعة جدًا ومفيدة جدًا. كما يمكنك استخدام العامل الشرطي الثلاثي ":?" كالتالي: condition ? exprIfTrue : exprIfFalse
سمات بواجهة مستخدم متناسقة
لنبقى في مجال تنسيق التطبيقات لكن من منظورٍ أوسع قليلًا. لقد استخدم معظمنا تطبيقات متنوعة، وربما سنتفق أن ما بجعل واجهة المستخدم جيدة هو تناسق العناصر. فمظهر مكوّنات واجهة المستخدم كحجم الخط وعائلته ولونه ستتبع أسلوبًا متناسقًا. ولإنجاز ذلك لابد من ربط قيم خصائص التنسيق المختلفة بمعاملات، وهذا ما يعرف ببناء السمات theming.
ربما يكون مصطلح "بناء السمات" مألوفًا لمستخدمي مكتبات بناء واجهات المستخدم المشهورة مثل Bootstrap وMaterial UI. وعلى الرغم من اختلاف طرق بناء السمات، إلا أنّ الفكرة الرئيسية ستبقى دائمًا استخدام المتغيرات مثل colors.primary
بدلًا من الأرقام السحرية مثل 0366dd# عند تعريف التنسيق، وهذا ما سيقود إلى زيادة في الاتساق والمرونة.
دعونا نلقي نظرة على طريقة التنفيذ العملي لبناء السمات في تطبيقنا. سنستخدم في عملنا العديد من النصوص وبتنسيقات مختلفة مثل حجم الخط ولونه. وطالما أنّ React Native لا تدعم التنسيق العام، لا بد من إنشاء مكوّنات Text
خاصة بنا لنبقي المحتوى النصي متسقًا. لنبدأ إذًا بإضافة شيفرة كائن "تهيئة التنسيق" التالية إلى الملف "theme.js" الموجود في المجلد "src":
const theme = { colors: { textPrimary: '#24292e', textSecondary: '#586069', primary: '#0366d6', }, fontSizes: { body: 14, subheading: 16, }, fonts: { main: 'System', }, fontWeights: { normal: '400', bold: '700', }, }; export default theme;
علينا الآن إنشاء الكائن Text
الفعلي الذي يستخدم قواعد تهيئة التنسيق. لننشئ إذًا الملف "Text.jsx" في المجلد "components" الذي يحتوي ملفات بقية المكوّنات، ولنضع فيه الشيفرة التالية:
import React from 'react'; import { Text as NativeText, StyleSheet } from 'react-native'; import theme from '../theme'; const styles = StyleSheet.create({ text: { color: theme.colors.textPrimary, fontSize: theme.fontSizes.body, fontFamily: theme.fonts.main, fontWeight: theme.fontWeights.normal, }, colorTextSecondary: { color: theme.colors.textSecondary, }, colorPrimary: { color: theme.colors.primary, }, fontSizeSubheading: { fontSize: theme.fontSizes.subheading, }, fontWeightBold: { fontWeight: theme.fontWeights.bold, }, }); const Text = ({ color, fontSize, fontWeight, style, ...props }) => { const textStyle = [ styles.text, color === 'textSecondary' && styles.colorTextSecondary, color === 'primary' && styles.colorPrimary, fontSize === 'subheading' && styles.fontSizeSubheading, fontWeight === 'bold' && styles.fontWeightBold, style, ]; return <NativeText style={textStyle} {...props} />; }; export default Text;
وهكذا نكون قد أنجزنا مكوّن النص الخاص بنا من حيث اللون وحجم الخط وكثافته لنستخدمه في أي مكان من تطبيقنا. يمكننا الحصول على تغييرات مختلفة في هذا المكوّن باستخدام خصائص مختلفة كالتالي:
import React from 'react'; import Text from './Text'; const Main = () => { return ( <> <Text>Simple text</Text> <Text style={{ paddingBottom: 10 }}>Text with custom style</Text> <Text fontWeight="bold" fontSize="subheading"> Bold subheading </Text> <Text color="textSecondary">Text with secondary color</Text> </> ); }; export default Main;
لك كامل الحرية في توسيع أو تعديل هذا المكوّن إن أردت. وقد يكون إنشاء مكوّنات نصية قابلة لإعادة الاستخدام تستخدم المكوّن Text
مثل Subheading
فكرة جيدة. حاول أيضًا أن توسع أو تعدل قواعد تهيئة السمة كلما تقدمنا في بناء تطبيقنا.
استخدام flexbox في تصميم واجهة التطبيق
سنعرّج أخيرًا في عرضنا لمواضيع تنسيق التطبيقات على تصميم الواجهات باستخدام flexbox. يعرف المطورون الذين ألفوا اللغة CSS أنّ flexbox لا يتعلق فقط بإطار العمل React Native، فقد يستعمل في حالات عدة لتطوير صفحات الويب أيضًا. وعمليًا لن يتعلم المطورون الذين يعرفون آلية عمل flexbox الكثير من هذه الفقرة، لكن دعونا على الأقل نراجع أو نتعلم أساسيات flexbox.
flexbox هو كيان لتصميم الواجهات يتألف من مكونين منفصلين هما: حاوية flex وضمنها مجموعة من عناصر flex. تمتلك حاوية flex مجموعة من الخصائص التي تتحكم بتوزيع العناصر. ولجعل مكوّن ما حاوية flex، لا بد من أن يمتلك خاصية التنسيق display
وقد أسندت لها القيمة "flex" وهي القيمة الافتراضية لهذه الخاصية. تظهر الشيفرة التالية طريقة تعريف حاوية flex:
import React from 'react'; import { View, StyleSheet } from 'react-native'; const styles = StyleSheet.create({ flexContainer: { flexDirection: 'row', }, }); const FlexboxExample = () => { return <View style={styles.flexContainer}>{/* ... */}</View>; };
ربما تكون الخصائص التالية أهم خصائص حاوية flex:
-
الخاصية flexDirection: وتتحكم بالاتجاه الذي ستأخذه عناصر flex عند ترتيبها ضمن الحاوية. وتأخذ هذه الخاصية القيم row وrow-reverse وcoulmn وcolumn-reverse. سيرتب row عناصر flex من اليسار إلى اليمين، بينما سيرتبها column من الأعلى للأسفل (القيمة الافتراضية). وستعكس القيم ذات اللاحقة reverse- اتجاه ترتيب العناصر.
-
الخاصية justifyContent: وتتحكم بمحاذاة عناصر flex على طول المحور الرئيسي للترتيب الذي تحدده الخاصية السابقة. تأخذ هذه الخاصية القيم flex-start (القيمة الافتراضية) وflex-end وcenter وspace-between وspace-around وspace-evenly.
-
الخاصية alignItems: وتقوم بوظيفة الخاصية السابقة لكن على المحور الآخر لترتيب العناصر. تأخذ هذه الخاصية القيم flex-start وflex-end وcenter وbaseline و stretch (القيمة الافتراضية).
لننتقل الآن إلى عناصر flex والتي أشرنا سابقًا على أنها محتواة ضمن حاوية flex. تمتلك هذه العناصر خصائص تتحكم بتنسيق وسلوك هذه العناصر بالنسبة لبعضها في نفس الحاوية. ولكي يكون المكوّن عنصر flex يكفي أن نجعله ابنا مباشرًا لحاوية flex:
import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; const styles = StyleSheet.create({ flexContainer: { display: 'flex', }, flexItemA: { flexGrow: 0, backgroundColor: 'green', }, flexItemB: { flexGrow: 1, backgroundColor: 'blue', }, }); const FlexboxExample = () => { return ( <View style={styles.flexContainer}> <View style={styles.flexItemA}> <Text>Flex item A</Text> </View> <View style={styles.flexItemB}> <Text>Flex item B</Text> </View> </View> ); };
إنّ أكثر خصائص العناصر استخدامًا هي flexGrow. حيث تقبل هذه الخاصية قيمة تحدد قدرة العنصر على النمو إن دعت الحاجة. فلو كانت قيمة هذه الخاصية لكل عناصر flex في حاوية هي 1، ستتقاسم هذه العناصر المساحة المتاحة للحاوية بالتساوي. بينما لو كانت قيمتها 0 لعنصر ما، فسيأخذ المساحة التي يتطلبها محتواه فقط، وسيترك المساحة الباقية لبقية العناصر.
ننصحك بدراسة المثال النموذجي Flexbox example، والذي يشكّل مثالًا رئيسًا أكثر تفاعلية لاستخدام flexbox في تنفيذ بطاقة بسيطة لها حاشية علوية وجسم وحاشية سفلية.
إقرأ تاليًا المقالة تعرف على CSS Flexbox وأساسيات استعماله لهيكلة صفحات الويب التي تحتوي على أمثلة مصوّرة شاملة عن استخدام flex. ومن الجيد أيضًا أن تجرب خصائص flexbox ضمن أرضية العمل Flexbox Playground لترى كيفية تأثير الخصائص على تصميم الواجهة. وتذكر أنّ أسماء الخصائص في React Native هي نفسها في CSS ما عدا نقطة التسمية بأسلوب سنام الجمل، إلا أن قيم هذه الخاصيات مثل flex-start وflex-end ستبقى نفسها.
اقتباسملاحظة: تختلف React Native عن CSS في بعض النواحي المتعلقة باستخدام flexbox. فالإختلاف الأهم هو أنّ القيمة الافتراضية للخاصية
flexDirection
في React Native هي column. كما يجدر الإشارة إلى أنّ اختصارات flex لا تقبل قيمًا متعددة في React Native. لتطلع أكثر على استخدام flexbox في تطبيقات React Native راجع توثيق React Native.
التمارين 10.4 - 10.5
10.4 شريط تنقل للتطبيق
سنحتاج قريبًا إلى التنقل بين الواجهات المختلفة للتطبيق، لذلك سنحتاج إلى شريط تنقل بين هذه الواجهات. أنشئ الملف "AppBar.jsx" وضعه في المجلد "components" ثم ضع الشيفرة التالية ضمنه:
import React from 'react'; import { View, StyleSheet } from 'react-native'; import Constants from 'expo-constants'; const styles = StyleSheet.create({ container: { paddingTop: Constants.statusBarHeight, // ... }, // ... }); const AppBar = () => { return <View style={styles.container}>{/* ... */}</View>; }; export default AppBar;
وبما أنّ المكوّن AppBar
سيمنع شريط الحالة من تغطية المحتوى، يمكن إزالة خاصية التنسيق marginTop
التي وضعناها للمكوّنMain
في الملف "Main.jsx". ينبغي أن يضم المكوّن AppBar
نافذة عنوانها "Repositories". اجعل النافذة مستجيبة للمس touchable باستخدام المكوّن TouchableWithoutFeedback، لكن لا حاجة الآن لتعالج الحدث onPress
. أضف المكوّن AppBar
إلى المكوّن Main
، بحيث يبقى المكوّن الأعلى في الواجهة. من المفترض أن يبدو المكوّن AppBar
شبيهًا للتالي:
إن لون خلفية التطبيق كما تظهره الصورة السابقة هو 24292e#، لكن يمكنك استخدام اللون الذي تشاء. يمكنك أيضًا تهيئة لون خلفية التطبيق ضمن قواعد تهيئة السمة، وسيصبح تغيير اللون أمرًا سهلًا إن احتجت لذلك. ومن الجيد أيضًا فصل شيفرة النافذة الموجودة في المكوّن AppBar
لتصبح ضمن مكوّن مستقل يمكن أن تسميه AppBarTab
، سيسهّل ذلك إضافة نوافذ أخرى مستقبلًا.
10.5 قائمة محسنة المظهر للمستودعات المقيمة
تبدو النسخة الحالية من تطبيق قائمة المستودعات المقيَّمة كئيبة المظهر. عدّل المكوّن RepositoryListItem
ليعرض أيضًا الصورة التمثيلية لمؤلف المستودع. ويمكنك إنجاز ذلك باستخدام المكوّن Image. ينبغي إظهار القيم العددية كعدد النجمات وعدد التشعبات التي تزيد عن 1000 برقم مرتبة الآلاف وبدقة فاصلة عشرية واحدة فقط تليها اللاحقة "k". فلو كان مثلًا عدد التشعبات هو 8439، ستُعرض على الشاشة القيمة "8.4k". حسِّن أيضًا المظهر الكلي للمكوّن بحيث تبدو قائمة المستودعات مشابهة للتالي:
إنّ لون الخلفية للمكوّن Main
في الصورة السابقة هو e1e4e8# بينما ستظهر خلفية للمكوّن RepositoryListItem
باللون الأبيض. كما يأخذ لون خلفية محدد اللغة القيمة 0366d6 #، وهي نفسها قيمة المتغيّر colors.primary
في إعدادات تهيئة السمة. لا تنس الاستفادة من المكوّن Text
الذي أنجزناه سابقًا. افصل المكوّن RepositoryListItem
-إن دعت الحاجة- إلى مكوّنات أصغر.
مسارات التنقل في React Native
سرعان ما سنحتاج إلى آلية للانتقال بين مختلف واجهات العرض في التطبيق كواجهة المستودعات وواجهة تسجيل الدخول، وذلك عندما نبدأ بتوسيعه. وكنا قد تعلمنا في القسم 7 استخدام المكتبة React router لتنفيذ مهمة التنقل والتوجه في تطبيقات الويب.
تختلف آلية التنقل في React Native قليلًا عن التنقل في تطبيقات الويب. ويظهر الاختلاف الرئيسي في عدم القدرة على الإشارة المرجعية للصفحات من خلال العناوين "URL" التي نكتبها في شريط تنقل المتصفح، ولا يمكننا كذلك التنقل بين الصفحات التالية والسابقة اعتمادًا على الواجهة البرمجية لتاريخ تنقلات المستخدم history API في المتصفح. لكن تبقى هذه الإشكالية مسألة تتعلق بواجهة التنقل التي نستخدمها.
يمكننا في ReactNative استخدام متحكمات المسار البنيوية React Router's core بالكامل كالخطافات والمكوّنات. لكن علينا استبدال BrowserRouter
الذي يستخدم في بيئة المتصفح بمقابله NativeRouter الملائم لإطار عمل React Native والذي تؤمنه المكتبة react-router-native. لنثبّت إذًا هذه المكتبة:
npm install react-router-native
سيؤدي استخدام المكتبة react-router-native إلى توقف عرض التطبيق ضمن متصفح ويب المنصة Expo، بينما ستعمل بقية طرق عرض التطبيق بشكل جيد. يمكن إصلاح هذه المشكلة بتوسيع إعدادات تهيئة المجمع webpack للمنصة Expo بحيث ينقل transpile الشيفرة المصدرية للمكتبة باستخدام Babel. ولتوسيع إعدادات التهيئة، لابدّ من تثبيت المكتبة expo/webpack-config@:
npm install @expo/webpack-config --save-dev
سننشئ بعد ذلك الملف "webpack.config.js" في المجلد الجذري للمشروع وضمنه الشيفرة التالية:
const path = require('path'); const createExpoWebpackConfigAsync = require('@expo/webpack-config'); module.exports = async function(env, argv) { const config = await createExpoWebpackConfigAsync(env, argv); config.module.rules.push({ test: /\.js$/, loader: 'babel-loader', include: [path.join(__dirname, 'node_modules/react-router-native')], }); return config; };
أعد تشغيل أدوات تطوير لكي تُطبق إعدادات تهيئة webpack الجديدة، وستجد أن المشكلة قد حُلّت. افتح الآن الملف "App.js" وأضف المكوّن NativeRouter
إلى المكوّن App
:
import React from 'react'; import { NativeRouter } from 'react-router-native'; import Main from './src/components/Main'; const App = () => { return ( <NativeRouter> <Main /> </NativeRouter> ); }; export default App;
بعد أن أنشأنا متحكمًا بالمسار، سنضيف أول مسار تنقل (وجهة- Route) في المكوّن Main
، وذلك ضمن الملف "Main.jsx":
import React from 'react'; import { StyleSheet, View } from 'react-native'; import { Route, Switch, Redirect } from 'react-router-native'; import RepositoryList from './RepositoryList'; import AppBar from './AppBar'; import theme from '../theme'; const styles = StyleSheet.create({ container: { backgroundColor: theme.colors.mainBackground, flexGrow: 1, flexShrink: 1, }, }); const Main = () => { return ( <View style={styles.container}> <AppBar /> <Switch> <Route path="/" exact> <RepositoryList /> </Route> <Redirect to="/" /> </Switch> </View> ); }; export default Main;
التمرينان 10.6 - 10.7
10.6 واجهة عرض تسجيل الدخول
سننفذ قريبًا نموذجًا لكي يصبح المستخدم قادرًا على تسجيل دخوله إلى التطبيق. لكن علينا قبل ذلك أن ننشئ واجهة عرض يمكن الوصول إليها من شريط التطبيق. أنشئ الملف "Signin.jsx" في المجلد "components"، ثم ضع الشيفرة التالية ضمنه:
import React from 'react'; import Text from './Text'; const SignIn = () => { return <Text>The sign in view</Text>; }; export default SignIn;
هيئ مسارًا وجهته المكوّن Signin
وضعه في المكوًن Main
. أضف بعد ذلك نافذة عنوانها النص "Sign in" في أعلى شريط التطبيق إلى جوار النافذة "Repositories". ينبغي أن يكون المستخدم قادرًا على التنقل بين النافذتين السابقتين بالضغط على عنوان كل نافذة
اقتباس
10.7 شريط تطبيق قابل للتمرير scrollable
طالما أننا سنضيف نوافذ جديدة إلى شريط التطبيق، فمن الجيد أن يكون قابلًا للتمرير أفقيًا عندما لا تتسع الواجهة لكل النوافذ. سنستخدم في ذلك المكوّن الأنسب لأداء المهمة وهو ScrollView. ضع النوافذ الموجودة في المكوّن AppBar
ضمن المكوّن ScrollView
:
const AppBar = () => { return ( <View style={styles.container}> <ScrollView horizontal>{/* ... */}</ScrollView> </View> ); };
عندما تُسند القيمة "true" إلى الخاصية horizontal العائدة للمكون ScrollView
، سيجعله قابلًا للتمرير أفقيًا عندما لا تتسع الشاشة للنوافذ التي يحتويها شريط التطبيق. وانتبه إلى ضرورة إضافة خصائص تنسيق ملائمة للمكوّن ScrollView
، بحيث تُرتَّب النوافذ على شكل صف "row" ضمن حاوية flex. للتأكد من إنجاز المطلوب أضف نوافذ جديدة إلى الشريط حتى لا تتسع الشاشة لها جميعًا، ولا تنس إزالة هذه النوافذ التجريبية بعد التحقق من إنجاز المهمة.
إدارة حالة النموذج
بعد أن حصلنا على حاضنة مناسبة لواجهة عرض تسجيل الدخول، ستكون المهمة التالية إنجاز نموذج لتسجيل الدخول. لكن قبل الشروع في ذلك، سنتحدث قليلًا عن النماذج وبمنظور أوسع.
يعتمد تنفيذ النماذج بشدة على إدارة الحالة. فقد يقدم لنا الخطاف useState
حلًا جيدًا للنماذج الصغيرة، لكن سيكون التعامل مع الحالة بنفس الأسلوب مرهقًا عندما يغدو النموذج أكثر تعقيدًا. ولحسن الحظ سنجد مكتبات تتعايش مع بيئة React لتسهيل عملية إدارة حالة النماذج ومنها المكتبة Formik.
تعتمد المكتبة Formik على مفهومي سياق العمل context والحقل field. يؤمن سياق العمل المكوّن Formik الذي يحتوي على حالة النموذج. وتتكون الحالة من معلومات عن حقول النموذج، وتتضمن هذه المعلومات مثلًا قيم الحقول والأخطاء الناتجة عن تقييم كلٍ منها. يمكن الإشارة إلى حالة الحقول باستخدام اسم الحقل عن طريق الخطاف useField أو المكوّن Field.
لنرى كيف سيعمل الأمر من خلال إنشاء نموذج لحساب "مؤشر كتلة الجسم" عند البشر:
import React from 'react'; import { Text, TextInput, TouchableWithoutFeedback, View } from 'react-native'; import { Formik, useField } from 'formik'; const initialValues = { mass: '', height: '', }; const getBodyMassIndex = (mass, height) => { return Math.round(mass / Math.pow(height, 2)); }; const BodyMassIndexForm = ({ onSubmit }) => { const [massField, massMeta, massHelpers] = useField('mass'); const [heightField, heightMeta, heightHelpers] = useField('height'); return ( <View> <TextInput placeholder="Weight (kg)" value={massField.value} onChangeText={text => massHelpers.setValue(text)} /> <TextInput placeholder="Height (m)" value={heightField.value} onChangeText={text => heightHelpers.setValue(text)} /> <TouchableWithoutFeedback onPress={onSubmit}> <Text>Calculate</Text> </TouchableWithoutFeedback> </View> ); }; const BodyMassIndexCalculator = () => { const onSubmit = values => { const mass = parseFloat(values.mass); const height = parseFloat(values.height); if (!isNaN(mass) && !isNaN(height) && height !== 0) { console.log(`Your body mass index is: ${getBodyMassIndex(mass, height)}`); } }; return ( <Formik initialValues={initialValues} onSubmit={onSubmit}> {({ handleSubmit }) => <BodyMassIndexForm onSubmit={handleSubmit} />} </Formik> ); };
لا يعتبر هذا المثال جزءًا من تطبيقنا، لذا لا حاجة لإضافة شيفرته إلى التطبيق. لكن يمكنك تجريب الشيفرة في Expo Snack مثلًا، وهو محرر لشيفرة React Native على شبكة الإنترنت على غرار JSFiddle وCodePen، ويمثل منصة مفيدة لتجريب الشيفرات بسرعة. كما يمكنك مشاركة Expo Snacks مع الآخرين بتحديد رابط إلى عملك أو تضمين الشيفرة على شكل مشغّل Snack ضمن أي صفحة ويب. ولربما قد صادفت مسبقًا مشغل Snack في مادة منهاجنا أو أثناء اطلاعك على توثيق React Native.
عرّفنا في المثال السابق سياق عمل Formik ضمن المكوّن BodyMassIndexCalculator
، وزودناه بالقيم الأولية وباستدعاء لإرسال محتويات النموذج submit callback. كما استخدمنا الخاصية initialValues لتزويد السياق بالقيم الأولية على شكل كائنات مفاتيحها أسماء الحقول وقيمها هي القيم الأولية المقابلة. كما تزودنا الصفة onSubmit باستدعاء إرسال محتويات النموذج، حيث ينفَّذ هذا الاستدعاء عندما تُستدعى الدالة handleSubmit
شريطة أن لا تظهر أية أخطاء تقييم لمحتويات الحقول. تُستدعى الدالة التي تمثل ابنا مباشرًا للمكوّن Formik
من خلال الخصائص props التي تحتوي معلومات متعلقة بالحالة، كما تحتوي أفعالًا كالدالة handleSubmit
.
يحتوي المكوّن BodyMassIndexForm
ارتباطات الحالة بين السياق وعناصر الإدخال النصيّة. ويُستخدم الخطاف useField للحصول على قيمة حقل أو تعديل قيمته. يقبل useField
وسيطًا واحدًا هو اسم الحقل ويعيد مصفوفة من ثلاث كائنات هي [الحقل field, البيانات الوصفية meta، المُساعدات helpers]. يحتوي الكائن field على قيمة الحقل، بينما يحتوي الكائن meta على المعلومات الوصفية للحقل كرسائل الخطأ التي قد يرميها. أما الكائن الأخير helpers، فيحتوي على الأفعال التي تُستخدم لتغيير حالة الحقل كالدالة setValue
. وتجدر الإشارة إلى أنّ المكوّن الذي يستخدم الخطاف لابدّ أن يكون ضمن سياق Formik. أي يجب أن يكون هذا المكوّن من أبناء المكوّن Formik
.
يمكنك الاطلاع على نسخة تفاعلية عن المثال السابق باسم Formik example على موقع Expo Snack.
لقد سبب استخدام الخطاف useField
مع المكوّن TextInput
شيفرة مكررة. لنعزل هذه الشيفرة ونضعها في مكوّن جديد باسم FormikTextInput
، ولننشئ مكوّنًا مخصصًا باسم TextInput
لإضافة مظهر مرئي أفضل لعنصر الإدخال النصي. لنثبّت أولًا Formik:
npm install formik
سننشئ الآن الملف "TextInput.jsx" في المجلد "components" ونضع فيه الشيفرة التالية:
import React from 'react'; import { TextInput as NativeTextInput, StyleSheet } from 'react-native'; const styles = StyleSheet.create({}); const TextInput = ({ style, error, ...props }) => { const textInputStyle = [style]; return <NativeTextInput style={textInputStyle} {...props} />; }; export default TextInput;
لننتقل إلى المكوّن FormikTextInput
الذي ينشئ رابطًا لحالة Formik بالمكوّن TextInput
. سننشئ الآن الملف "FormikTextInput.jsx" في المجلد "components" ونضع فيه الشيفرة التالية:
import React from 'react'; import { StyleSheet } from 'react-native'; import { useField } from 'formik'; import TextInput from './TextInput'; import Text from './Text'; const styles = StyleSheet.create({ errorText: { marginTop: 5, }, }); const FormikTextInput = ({ name, ...props }) => { const [field, meta, helpers] = useField(name); const showError = meta.touched && meta.error; return ( <> <TextInput onChangeText={value => helpers.setValue(value)} onBlur={() => helpers.setTouched(true)} value={field.value} error={showError} {...props} /> {showError && <Text style={styles.errorText}>{meta.error}</Text>} </> ); }; export default FormikTextInput;
يمكننا بعد استخدام المكوّن FormikTextInput
أن نعيد كتابة المكوّن BodyMassIndexForm
في المثال السابق كالتالي:
const BodyMassIndexForm = ({ onSubmit }) => { return ( <View> <FormikTextInput name="mass" placeholder="Weight (kg)" /> <FormikTextInput name="height" placeholder="Height (m)" /> <TouchableWithoutFeedback onPress={onSubmit}> <Text>Calculate</Text> </TouchableWithoutFeedback> </View> ); };
وكما نرى سيقلص إنشاء المكوّن FormikTextInput
الذي يعالج ارتباطات حالة Formik بالمكوّن TextInput
الشيفرة اللازمة. ومن الجيد أن تكرر نفس العملية إن استخدَمَت نماذج Formik التي لديك مكونات إدخال بيانات.
التمرين 10.8
10.8 نموذج تسجيل الدخول
أنجز نموذجًا لتسجيل الدخوّل يرتبط بالمكوّن SignIn
الذي أضفناه سابقًا في الملف"SignIn.jsx". يجب أن يحتوي النموذج مربعي إدخال نصيين، أحدهما لاسم المستخدم والآخر لكلمة السر. كما يجب أن يحتوي زرًا لتسليم بيانات النموذج. لا حاجة لكتابة دالة الاستدعاء onSubmit
، بل يكفي إظهار قيم الحقول باستخدام الأمر console.log
عند تسليم البيانات:
const onSubmit = (values) => { console.log(values); };
تذكّر أن تستخدم المكوّن FormikTextInput
الذي أنشأناه سابقّا. كما يمكنك استخدام الخاصية secureTextEntry في المكوّن TextInput
لحجب كلمة السر عند كتابتها.
سيبدو نموذج تسجيل الدخول مشابهًا للتالي:
تقييم النموذج
تقدم Formik مقاربتين لتقييم النماذج: دالة تقييم أو تخطيط تقييم. فدالة التقييم: هي دالة تُمرر للمكون Formik
كقيمة للخاصية validate. حيث تستقبل الدالة قيم حقول النموذج كوسطاء وتعيد كائنًا يحتوى الأخطاء المحتملة الخاصة بكل حقل. أما تخطيط التقييم فيمرر إلى المكوّن Formik كقيمة للخاصية validationSchema. ويمكن إنشاء هذا التخطيط باستخدام مكتبة تقييم تُدعى Yup. لنثبّت هذه المكتبة إذًا:
npm install yup
كمثال على ما أوردنا، سننشئ تاليًا تخطيط تقييم لنموذج "مؤشر كتلة الجسم" الذي أنجزناه سابقًا. نريد أن نتحقق أنّ قيمتي كتلة الجسم والطول موجودتان، وأنهما قيم عددية. كذلك نريد أن نتحقق أنّ كتلة الجسم أكبر أو تساوي1، وأنّ الطول أكبر أو يساوي 0.5. تمثل الشيفرة التالية طريقة تنفيذ التخطيط:
import React from 'react'; import * as yup from 'yup'; // ... const validationSchema = yup.object().shape({ mass: yup .number() .min(1, 'Weight must be greater or equal to 1') .required('Weight is required'), height: yup .number() .min(0.5, 'Height must be greater or equal to 0.5') .required('Height is required'),}); const BodyMassIndexCalculator = () => { // ... return ( <Formik initialValues={initialValues} onSubmit={onSubmit} validationSchema={validationSchema} > {({ handleSubmit }) => <BodyMassIndexForm onSubmit={handleSubmit} />} </Formik> ); };
تجري عملية التحقق افتراضيًا عند حدوث أي تغيير في قيم حقول النموذج، وكذلك عند استدعاء الدالة handleSubmit
. فإن أخفق التحقق، لن تُستدعى الدالة التي تمرر إلى الكائن Formik
عبر الخاصية onSubmit
يعرض المكوًن FormikTextInput
الذي أنشأناه سابقًا رسائل الخطأ المتعلقة بالحقل إن وقع الخطأ الموافق لها ولُمس الحقل. أي في الحالة التي يتلقى فيها الحقل تركيز الدخل ثم يفقده:
const FormikTextInput = ({ name, ...props }) => { const [field, meta, helpers] = useField(name); // Check if the field is touched and the error message is present const showError = meta.touched && meta.error; return ( <> <TextInput onChangeText={(value) => helpers.setValue(value)} onBlur={() => helpers.setTouched(true)} value={field.value} error={showError} {...props} /> {/* Show the error message if the value of showError variable is true */} {showError && <Text style={styles.errorText}>{meta.error}</Text>} </> ); };
التمرين 10.9
10.9 تقييم نموذج تسجيل الدخول
تحقق أنّ حقلي كلمة السر واسم المستخدم في نموذج تسجيل الدخول إجباريين. وتذكر أن لا تستدعي الدالة onSubmit
التي أنجزناها في التمرين السابق إن أخفق تقييم النموذج.
سيعرض المكوّن FormikTextInput
حاليًا رسالة خطأ إن حدث خطأ في الحقل الذي تم لمسه، عزز مظهر هذه الرسالة بجعل نصها أحمر اللون.
بالإضافة إلى رسالة الخطأ، إجعل إطار حقل الإدخال أحمر اللون ليشير ذلك إلى وقوع خطأ ضمنه. وتذكر أن المكوّن FormikTextInput
سيعطي القيمة "true" للخاصية error
العائدة للمكوّن TextInput
عند حدوث أية أخطاء ضمن الحقل. يمكنك استعمال قيمة هذه الخاصية لإضافة تنسيق شرطي للمكوّن TextInput
.
سيبدو شكل نموذج تسجيل الدخول عند حدوث خطأ في أحد الحقول قريبًا من الشكل التالي:
اقتباسملاحظة: إن الرقم المرجعي للون الأحمر المستخدم هنا هو d73a4a#.
الشيفرة الخاصة بمنصة
من أعظم ميزات استخدام React Native هي أننا لن نهتم بالمنصة التي سيعمل عليها التطبيق سواء Android أو iOS. لكن، قد تصادفنا حالات نضطر فيها إلى تنفيذ شيفرة خاصة بمنصة محددة. من هذه الحالات مثلًا، كتابة مكوّنات بطرق مختلفة لتلائم منصات مختلفة.
يمكن الوصول إلى المنصة التي يستخدمها المستخدم من خلال الثابت Platform.OS
:
import { React } from 'react'; import { Platform, Text, StyleSheet } from 'react-native'; const styles = StyleSheet.create({ text: { color: Platform.OS === 'android' ? 'green' : 'blue', }, }); const WhatIsMyPlatform = () => { return <Text style={styles.text}>Your platform is: {Platform.OS}</Text>; };
يأخذ هذا الثابت إحدى القيمتين: android أو ios. يمكن كتابة شيفرة مخصصة لمنصة أيضًا مستفيدين من التابع Platform.select
. إذ يمرر للتابع كائن قد تأخذ مفاتيحه إحدى القيم التالية: ios أو android أو native أو default، ويعيد القيمة الأكثر ملائمة للمنصة التي يعمل عليها المستخدم. يمكننا إعادة كتابة المتغير styles
في المثال السابق باستعمال التابع Platform.select
كالتالي:
const styles = StyleSheet.create({ text: { color: Platform.select({ android: 'green', ios: 'blue', default: 'black', }), }, });
وبالإمكان أيضًا استخدام التابع Platform.select
لطلب مكوّن خاص بمنصة محددة:
const MyComponent = Platform.select({ ios: () => require('./MyIOSComponent'), android: () => require('./MyAndroidComponent'), })(); <MyComponent />;
إنّ الطريقة الأكثر تقدّمًا لكتابة وإدراج مكوّنات خاصة بمنصة محددة أو أية أجزاء من الشيفرة، هي استخدام ملفات لها إحدى اللاحقتين "ios.jsx." أو "android.jsx.". وانتبه إلى إمكانية استخدام أية لاحقة يمكن للمجمع أن يميزها مثل "js.". إذ يمكن مثلًا أن ننشئ ملفات باسم "Button.ios.jsx" أو باسم "Button.android.jsx" بحيث نستطيع إدراجها في شيفرتنا كالتالي:
import React from 'react'; import Button from './Button'; const PlatformSpecificButton = () => { return <Button />; };
وهكذا ستضم حزمة Android للتطبيق المكوّن المعرّف في الملف "Button.android.jsx"، بينما تضم حزمة iOS المكوّن المعرّف في الملف "Button.ios.jsx".
التمرين 10.10
10.10 خط خاص بمنصة محددة
حُددت عائلة الخط الذي نستخدمه حاليًا في تطبيقنا بأنها "System" ضمن إعدادات تهيئة السمة الموجودة في الملف "theme.js". استخدم بدلًا من الخط "system" خطًا من خطوط العائلة Sans-serif ليكون خطًا خاصًا بكل منصة. استخدم الخط "Roboto" في منصة Android والخط Arial في منصة iOS. يمكن أن تُبقي "System" مثل خط افتراضي.
هذا هو التمرين الأخير في هذا الفصل، وقد حان الوقت لتسليم التمارين إلى GitHub والإشارة إلى أنك أكملتها في منظومة تسليم التمارين. انتبه إلى وضع الحلول في القسم 2 ضمن المنظومة.
ترجمة -وبتصرف- للفصل React Native basics من سلسلة Deep Dive Into Modern Web Development.
اقرأ أيضًا
- المقال السابق: مدخل إلى React Native
- مدخل إلى التحريك في React Native
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.