چگونه یک کتابخانه اندروید در React Native بسازیم؟ – راهنمای جامع پیشرفته


اگر یک توسعهدهنده جاوا اسکریپت باشید و از شما خواسته شود که در بخش native در فریمورک React Native عمیق شوید، احتمالاً با مشکلاتی مواجه خواهید شد. ما در این نوشته با نمایش یک روش آسان برای ایجاد و استفاده از کدهای native که میتوانند یک SDK جاوا را به شکل یک کتابخانه کپسولهسازی کنند، نشان میدهیم که این کار به هیچ وجه دشوار نیست. مستندات راهاندازی و اجرای کدهای native در فریمورک React Native کاملاً مفید است. با این وجود گرد هم آوردن همه این بخشهای مختلف به صورت یک کتابخانه کارآمد، میتواند کاری چالشبرانگیز و زمانبر باشد.
به همین دلیل این ریپازیتوری گیتهاب (+) برای کمک به راهاندازی و اجرای موارد مورد نیاز برای توسعه کتابخانه آماده شده است که شامل موارد زیر است:
- اجرای یک Promise در جاوا از یک اپلیکیشن React Native و تغییر دادن حالت بر مبنای نتیجه.
- اجرای یک متد در جاوا و گوش دادن به یک callback برای پاسخ (DeviceEventEmitter). این وضعیت شبیه رفتاری است که هنگام پوشش SDK-های شخص ثالث نیاز داریم.
- تست کردن با Jest.
- محیط توسعه سریع با یک اپلیکیشن تست.
- بهترین رویهها از react-native-create-bridge و react-native-create-library.
بهترین روش برای افزایش سرعت توسعه یک کتابخانه، این است که شروع به این کار بکنیم. بنابراین در ادامه به توضیح شیوه ایجاد boilerplate میپردازیم. کد انتهای هر مرحله را میتوانید در این ریپو (+) زیر «STEP_NUMBER» مشاهده کنید.
گام اول – جاوا اسکریپت و ساختار کتابخانه
پیش از هر چیز بهتر است بررسی کنیم که آیا واقعاً به یک کتابخانه نیاز داریم یا نه. اگر میخواهید حجم پایینی از کد native را که ارتباط تنگاتنگی با اپلیکیشن شما دارد ایجاد کنید، سریعترین و بهترین روش ایجاد یک native داخل اپلیکیشن است. بدین منظور react-native-create-bridge نقطه شروع بسیار خوبی محسوب میشود. ما قصد داریم از کدی که در ادامه معرفی خواهیم کرد استفاده کنیم. اگر در این خصوص دچار شک هستید، میتوانید از همین مسیر کار را آغاز کنید و از گامهای بعدی این نوشته الهام گرفته و به رفع مشکلات بپردازید. چون تبدیل یک ماژول به کتابخانه کار دشواری نیست.
ما قصد داریم یک کتابخانه مجزا ایجاد کنیم که بتوان در اپلیکیشنهای مختلف از آن استفاده کرد و به احتمال بالا مقادیر بالایی از یک SDK نیتیو را نیز شامل میشود. برای نمونه یک SDK از سوی سختافزار سفارشی اندروید ارائه میشود.
کار خود را با ابزار توصیه شده از سوی مستندات ریاکت نیتیو (+) آغاز میکنیم:
$ npm install -g react-native-create-library $ react-native-create-library TestLib
بدین ترتیب پایه مناسبی برای کتابخانه به دست میآوریم، با این وجود، شاید همچنان حس کنید که در مورد چگونگی گرد هم آوردن این بخشها سردرگم هستید. برای این که این مسئله روشن شود، در ادامه این مرحله قصد داریم روی ساختار کد کتابخانه خود متمرکز شده و یک اپلیکیشن نمونه برای تست ایجاد کنیم.
اپلیکیشن نمونه
در آغاز باید چند کار مدیریتی انجام دهیم. یک پوشه به نام library ایجاد کرده و همه محتوای پوشه TestLib را به آن انتقال میدهیم. سپس دستور react-native init ExampleApp را برای تست کردن اپلیکیشن خود اجرا میکنیم.
اینک میتوانیم چگونگی استفاده از کتابخانه خود را با تلاش برای گرفتن مقداری از آن تعریف کنیم. تغییراتی که باید در App.js ایجاد شود را در ادامه مشاهده میکنید:
- import RNTest from 'react-native-test-lib’ - کتابخانه ما
- state – برای نگهداری پاسخ خالص از کد نیتیو ما.
- ()getNativeResult که یک تابع ناهمگام است و ابتدا state.nativeResult را به صورت loading… تنظیم میکند و در ادامه به صورت یک خطا با پاسخی از Promise به نام ()RNTest.getValaue تعیین میشود.
- ما در JSX خود مقدار this.state.nativeResult نمایش داده و یک دکمه برای فراخوانی ()getNativeResult اضافه میکنیم.
همچنین باید package.json خود را ویرایش کنیم تا شامل کتابخانه ما که هم اینک در App.js ایمپورت کردیم باشد و همراه با آن یک روش برای بهروزرسانی این کتابخانه در هنگام ایجاد تغییرات داشته باشیم. در ادامه اسکریپت updateLib ماژولهای گره را حذف کرده و کش را پاک میکند. سپس بستههای ما مجدداً نصب شده و React Native packager آغاز میشود.
DEPENDENCY: "react-native-test-lib": "../library" SCRIPT: “updateLib”: “rm -rf node_modules/ && yarn --reset-cache && yarn start”
البته میتوان این وضعیت را بدون نیاز به حذف همه node_modules نیز بهینهسازی کرد. همچنین لازم به ذکر است که شما باید در صورتی که کد نیتیو تغییر یابد، اپلیکیشن خود را rebuild کرده و نصب کنید.
ایجاد چارچوب کتابخانه
هدف اصلی ما این است که لایههای کاملاً تعریف شدهای از انتزاع را در تمام مسیر از Native تا جاوا اسکریپت ایجاد کنیم. این ساختار مستلزم تأیید این نظر است که نوشتن جاوا اسکریپت به کدنویسی جاوا ترجیح دارد.
با در نظر داشتن این قاعده یک پوشه testBridge ایجاد میکنیم که شامل همه کدهای جاوا اسکریپت است.
> testBridge > __tests__ -> Will contain our tests > bridgeOperations -> Operations available in native getValue.js -> Calls the getValue native code index.js -> Defines the bridge and exports operations > library -> Contains the features of our library index.js -> Crafts the library API index.js -> exports our library to the world ?
اگر توضیح کد فوق را از انتها آغاز کنیم، testBridge/bridgeOperations/index یک بریج تعریف میکند که مورد استفاده قرار میدهیم. در این مورد NativeModules.RNTestLib نام بریج است و آن را به همه bridgeOperation-ها (مانند getValue) ارسال میکنیم. اینها متدهای جاوا هستند که هر یک در فایل خود تعریف شدهاند. دو دلیل برای این مسئله وجود دارد: نخست این که قصد داریم از توانایی شبیه سای بریج برای تست کردن استفاده کنیم و دوم این که بررسی میکنیم اگر کد نیتیو مورد نظر پیچیده باشد و عملیات به پاکسازی نیاز داشته باشد چه رخ میدهد. برای نمونه فرض کنید یک متد جاوا که متد callback مجزایی دارد را اجرا کنیم و از DeviceEventEmitter برای ارسال مقدار callback بهره بگیریم. در این حالت مطمئناً بسیار بهتر است کد کتابخانه یک promise دریافت کند و با سازوکار درونی کد نیتیو و emmiter رویداد، سر و کار نداشته باشد.
در ادامه یک API که در testBridge/library/index ایجاد شده است را اکسپورت میکنیم و این پوشه ممکن است شامل چندین فایل باشد که ویژگیهای کتابخانه ما مانند الحاق چند bridgeOperations به API پاک را ارائه میکنند. به عنوان مثال فرض کنید میخواهید کتابخانه شما یک تابع connect را اکسپورت کند؛ اما میخواهید این کار در دو bridgeOperation به نامهای checkPermission و connect اجرا شود. این همان جایی است که فایلهای اجرایی این عملیات در آن قرار دارند. این وضعیت در نمودار زیر جمعبندی شده است:
آن چه در این جا ایجاد کردهایم، سطوح مشخصی از انتزاع از کد نیتیو است. bridgeOperations/ باید کارکرد درونی کد نیتیو ما را درک کند تا بتواند رابط تمیز جاوا اسکریپت را اکسپورت کند؛ اما library/ جایی است که ویژگیهایی از bridgeOperations ایجاد میشوند تا API اپلیکیشن ما مورد استفاده قرار دهد.
اینک بهترین زمان برای بررسی ساختار ماژول و آشنایی کامل با آن است. شما میتوانید اپلیکیشن نمونه را اجرا کنید، دکمه را فشار دهید و منتظر پاسخی از promise ما به نام bridgeOperations/getValue.js باشید. همه چیزهایی که تا این جا بررسی کردیم در شاخه STEP-ONE کد منبع ما قابل مشاهده است.
گام دوم – یکپارچهسازی نیتیو
اینک زمان آن رسیده است که Promise موجود در getValue.js را جعل کنیم تا یک رشته که در کد نیتیو تنظیم میکنیم را به دست آوریم. توصیه شده است که از اندروید استودیو برای تغییرات نیتیو زیر در library/android استفاده شود.
تغییرات کتابخانه
اپلیکیشن نمونه ما در آخرین نسخه از React Native است و از این رو باید کتابخانه build.gradle خود را بهروزرسانی کنیم. بدین منظور کافی است دو build.gradles را با هم تطبیق بدهیم. در این جا فرض میشود که اپلیکیشن از کتابخانه ما و ویژگیهایی که قرار است یکپارچهسازی شوند، استفاده میکند. بنابراین یک همگامسازی Gradle اجرا کنید تا مطمئن شوید که همه چیز آماده است.
سپس کد نیتیو را میتوانید در مسیر java/[…]/RNTestLibModule.java مشاهده کنید. این همان جایی است که در زمان یکپارچهسازی ویژگیهای نیتیو و SDK-ها اغلب زمان خود را در آن سپری میکنید. فایل boilerplate برای کتابخانه react-native-create-library در این جا کمی کوچک به نظر میرسد ، چون کارکردی که ما میخواهیم یک نقطه شروع مناسب محسوب میشود؛ اما جامعیت آن کمتر از مثالی است که در مستندات ریاکت نیتیو (+) ارائه شده است. اگر react-native-create-bridge را که قبلاً مورد اشاره قرار دادیم، تست کرده باشید، میدانید که این کتابخانه نقطه شروع خوبی است محسوب میشود و از این رو از module.java آن به عنوان یک نقطه شروع استفاده میکنیم. همچنین به راحتی میتوان از لینکهای آن به مثالهای دیگر در مستندات ریاکت نیتیو بهره جسته و از این رو به این فایل مواردی را صرفاً اضافه میکنیم و چیزی از آن حذف نخواهیم کرد. کافی است نامهای کلاسها را برای مطابقت با کتابخانه بهروزرسانی کنیم.
✅ بهترین رویهها
در نهایت یک Promise به نام getValue در ماژول خود ایجاد میکنیم تا جایگزین مواردی که شبیهسازی کردیم بشود:
import com.facebook.react.bridge.Promise; @ReactMethod // Notates a method that should be exposed to React public void getValue(final Promise promise) { promise.resolve("A real native value"); }
به خاطر داشته باشید که جاوا اسکریپت ما هم اینک همان Promise شبیهسازی شده را باز میگرداند و از این رو باید آن را عوض کنیم تا نوع نیتیو را بازگرداند:
const getValue = bridge => bridge.getValue();
همچنین باید نام ماژولهای نیتیو خود را که بریج را در bridgeOperations/index تعریف کردهایم، بهروزرسانی کنیم و به NativeModules.RNTestLibModule تغییر دهیم تا با نام کلاسی که تعیین کردیم مطابق باشد.
تغییرات اپلیکیشن نمونه
برای آغاز کار باید به کتابخانهای لینک شویم که به اپلیکیشن ما میگوید کد نیتیو ما را کامپایل کرده و در اختیار ما قرار دهد. به این منظور میتوانیم از فایل readme که به صورت خودکار در library/ ایجاد شده استفاده کنیم.
اینک دستور yarn updateLib را اجرا کرده و اپلیکیشن را مجدداً روی دستگاه خود نصب کنید تا با فشردن دکمه عبارت «A real native response» نمایش یابد.
✅ اجرای یک promise در جاوا
✅ داشتن یک محیط توسعه سریع با اپلیکیشن تست
کدی که در این مرحله استفاده کردیم را میتوانید در شاخه STEP-TWO ریپازیتوری گیتهاب ما مشاهده کنید. یک تمرین خوب هنگام بررسی اپلیکیشن این است که یک مقدار به کد نیتیو ارسال کنید و آن را به ریاکت بازگردانید تا یک حلقه کامل ایجاد شود.
گام سوم
در نهایت ما باید به چگونگی مدیریت گوش دادن به رویدادها در زمان تست کتابخانه خود بپردازیم.
ارسال رویدادها
به محض این که وارد حیطه جاوا میشویم و SDK-های شخص ثالث را ادغام میکنیم به زودی بر ما مشخص میشود که باید از جاوا اسکریپت بخواهیم یک متد نیتیو را اجرا کند که یک callback دارد و مقداری را باز میگرداند. خوشبختانه این وضعیت با استفاده از DeviceEventEmitter به سادگی قابل پیادهسازی است. در ادامه متد requestDeviceID را میبینید که آن چه میتواند یک callback از یک SDK باشد را تحریک میکند و میتوانیم مقداری که میخواهیم را ارسال کنیم.
import com.facebook.react.bridge.Arguments; @ReactMethod public void requestDeviceId() { // A method to request the device ID, below you could be calling an SDK implementation // Remember to consider error handing here to void app crashes deviceID("10001"); deviceID("10001"); } private void deviceID(String id){ // This might be a method within a class that implements the SDK you're using // We use Arguments.createMap to build an object to return WritableMap idData = Arguments.createMap(); idData.putString("id", id); emitDeviceEvent("device-id", idData); }
بخش نیتیو در این جا به پایان میرسد. اینک نوبت جاوا اسکریپت رسیده است. یک فایل جدید به نام requestDeviceId.js درون bridgeOperations ایجاد کنید. این فایل به ما امکان میدهد که فراخوانی متد آغازین requestDeviceId را به رویداد ‘device-id’ ارسالی اتصال دهیم و همه آنها را در یک promise تمیز برای استفاده کتابخانه خودمان پوشش دهیم.
اینک باید فایل bridgeOperations را بهروزرسانی کنیم. در این بخش از همان ساختاری که برای ایجاد بریج eventEmitter استفاده کردیم بهره میگیریم. چون این ساختار به کتابخانه ما امکان میدهد که یک eventEmitter اصلی داشته باشد و به ما نیز امکان میدهد که آن را برای تست کردن شبیهسازی کنیم. فراموش نکنید که عملیات جدیدی که ایجاد کردیم را اکسپورت کنید:
const requestDeviceId = (bridge, eventEmitter) => { bridge.requestDeviceId(); return new Promise((resolve, reject) => { const listener = eventEmitter.addListener('device-id', (response) => { resolve(response); listener.remove(); }); // Could add a listener for errors here too }); }; export default requestDeviceId;
اینک کد خود را در اپلیکیشن نمونه تست میکنیم. به این منظور تابع زیر را برای درخواست id دستگاه اضافه کردهایم:
import { NativeModules, NativeEventEmitter } from 'react-native'; import getValue from './getValue'; import requestDeviceId from './requestDeviceId'; const bridge = NativeModules.RNTestLibModule; const eventEmitter = new NativeEventEmitter(bridge); export default { getValue: () => getValue(bridge), requestDeviceId: () => requestDeviceId(bridge, eventEmitter), };
همراه با آن یک حالت به نام deviceId، یک دکمه برای فراخوانی این تابع و یک تگ <Text> برای نمایش حالت اضافه کردهایم. فشردن این دکمه باعث میشود که deviceId در کد نیتیو ما نمایش یابد. اینک ما شروع به اجرای یکپارچه کل گردش کار خود کردهایم.
✅ گوش کردن به callback-های جاوا
ایجاد یک ویژگی
در این مرحله زمان نمایش ایدهای فرا رسیده است که در پوشه library پیادهسازی کردهایم. بدین منظور دو متد نیتیو را در یک ویژگی تمیز ترکیب کرده و یک API کتابخانه تشکیل میدهیم.
یک پوشه جدید به نام coolFeature.js در کتابخانه ایجاد میکنیم. این پوشه جایی است که دو عملیات مورد نظر با هم ترکیب میشوند. کد آن به صورت زیر است:
const coolFeature = async (bridgeOperations) => { try { const value = await bridgeOperations.getValue(); // Our emitter sends an object by creating a writable map // Below we just destructure that object const { id } = await bridgeOperations.requestDeviceId(); return `Device: ${id}, says you are seeing ${value} `; } catch (e) { throw (new Error(e)); } }; export default coolFeature;
همچنین باید coolFeature خودمان را به عنوان بخشی از کتابخانه در library/index.js اکسپورت کنیم. بدین منظور کافی است مقدار زیر را به شیء اکسپورت شده اضافه کنیم:
coolFeature: () => coolFeature(bridgeOperations)
اکنون مراحلی که برای ایجاد رابط کاربری deviceId طی کردیم را برای رابط کاربری cool feature تکرار میکنیم.
زمانی که دکمه را فشار دهید میبینید که کتابخانه مورد نظر هر دو عملیات قبلاً مستقل از هم را به صورت یک ویژگی کاملاً به هم پیوسته باز میگرداند. این وضعیت نشان دهنده قدرت این ساختار است و نشان میدهد که چگونه میتوان کارکرد درونی کد نیتیو را به صورت انتزاع درآورد. همه چیزهایی که در library/ قرار دارند به سهولت از سوی توسعهدهندههای جاوا اسکریپت قابل دسترسی هستند و درک bridgeOperates/ آسان است؛ اما درک چگونگی کارکرد درونی آن صرفاً برای افرادی که به بررسی کد نیتیو و SDK-ها علاقهمند هستند مورد نیاز است.
تستها
در آخرین مرحله اقدام به تست کتابخانه خود میکنیم. برای این که بتوانیم از Jest استفاده کنیم باید فایل .babelrc را در سطح بالای library/ با محتوای زیر درج کنیم. دلیل نیاز ما به آن این است که به Babel بگوییم میخواهیم کد Jest خود را transpile کنیم:
{ "presets": ["env"] }
برای شروع یک پوشه به نام __test__ شامل library/coolFeature.js/ ایجاد میکنیم. ما قصد داریم یک تست برای شبیهسازی پاسخها از کد نیتیو خود ایجاد کنیم تا مطمئن شویم که coolFeature یک رشته با ساختار صحیح به کاربر کتابخانه بازگشت میدهد. کد زیر را اضافه کرده و تست را اجرا کنید:
In the within the following folders __tests__/library : import coolFeature from '../../library/coolFeature'; const responses = { value: 'A real native value', requestId: { id: '10001' }, }; const bridgeOperations = { getValue: jest.fn().mockReturnValueOnce(responses.value), requestDeviceId: jest.fn().mockReturnValueOnce(responses.requestId), }; describe('Cool feature', () => { test('is returning a correctly structured string', async () => { try { const response = await coolFeature(bridgeOperations); expect(response).toBe(`Device: ${responses.requestId.id}, says you are seeing ${responses.value}`); } catch (e) { throw (e); } }); });
این تنها یک نمونه از چگونگی اجرای تست ویژگیهای کتابخانه از طریق شبیهسازی bridgeOperations است. شما میتوانید با شبیهسازی بریج و eventEmitter از همین مفهوم ضروری برای bridgeOperations استفاده کنید.
✅ تست کردن کتابخانه
اگر میخواهید این کتابخانه را به اپلیکیشن دیگری اضافه کنید، در حالتی که روی یک ریپازیتوری خصوصی گیتهاب میزبانی شده باشد، میتوانید یک وابستگی به صورت زیر به پروژه خود اضافه کنید:
"react-native-test-lib": "git+ssh://git@github.com/COMPANY/REPO.git#BRANCH",
بدین ترتیب و به همین سادگی یک پوشش به پروژه اضافه میشود. شما باید ابزارهای مورد نیاز برای ساخت یک کتابخانه نیتیو تمیز را درک کرده و در اختیار داشته باشید تا بتوانید آن را در معرض SDK-های شخص ثالث قرار داده با هر کار دیگری که تصور میکنید را انجام دهید. کد این مرحله در شاخه STEP-THREE ریپو گیتهاب پروژه قرار دارد.
اگر این مطلب برایتان مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای طراحی و توسعه پروژه های وب
- آموزش مقدماتی فریمورک React Native برای طراحی نرم افزارهای اندروید و iOS با زبان جاوااسکریپت
- مجموعه آموزشهای برنامه نویسی اندروید
- چگونه با React Native اپلیکیشن اندرویدی بنویسیم؟ — به زبان ساده
- آموزش فریمورک React — ساخت یک سیستم طراحی با قابلیت استفاده مجدد
- انیمیشن های اسکرول افقی در React Native — به زبان ساده
- ۶ روش آسان برای سرعت بخشیدن به اپلیکیشن های React Native
==