مكونات واجهة المستخدم الأصيلة (Android) في React Native

من موسوعة حسوب


هناك الكثير من أدوات واجهة المستخدم (UI widgets) الأصيلة الجاهزة للاستخدام في أحدث التطبيقات، بعضها جزء من المنصّة، والبعض الآخر متاح كمكتبات تابعة لجهات طرف ثالث (third-party libraries)، إضافة إلى ما تستخدمه أنت كذلك. يحتوي React Native على العديد من مكوّنات المنصّة الأكثر أهمية، وهي جاهزة للاستخدام، مثل ScrollView وTextInput، ولكن ليس جميعها، وبالطبع، فهذا لا يشمل تلك التي قد تكون كتبتها بنفسك. لحسن الحظ، من السهل جدًا تغليف هذه المكونات الحالية لإدماجها بسلاسة مع تطبيق React Native الخاص بك.

على غرار صفحة الوحدات الأصيلة، فهذا الدليل كذلك أكثر تقدمًا، ويَفترِض أنك معتاد إلى حد ما على برمجة Android SDK. سيوضح لك هذا الدليل كيفية إنشاء مكون واجهة مستخدم أصيل، وسيرشدك إلى كتابة مجموعة فرعيّة من مكون ImageView الموجود في مكتبة React Native الأساسية (the core React Native library).

ImageView كمثال

في هذا المثال، سنتعرف على متطلبات الإجراء (implementation requirements) للسماح باستخدام ImageView في JavaScript.

تُنشأ العروض الأصيلة وتُعالَج عن طريق توسيع ViewManager أو SimpleViewManager بشكل أكثر شيوعًا. يعتبر SimpleViewManager مناسبًا في هذه الحالة لأنه يطبق خاصيات شائعة مثل لون الخلفية والتعتيم وتخطيط Flexbox.

هذه الأصناف الفرعية هي مفردات (singletons) أساسا، أي تُنشأ نسخة واحدة فقط لكل منها بواسطة الجسر. وهي تُزوِّد العروضَ الأصيلة إلى NativeViewHierarchyManager، والذي يُفوِّض لها مرة أخرى لتعيين وتحديث خاصيات العروض حسب الضرورة. عادةً ما يكون مدراء العروض (أصناف ViewManager) مفوَّضين للعروض، مُرسلةً الأحداث مرة أخرى إلى JavaScript عبر الجسر.

تزويدُ عرضِِ (Vending a view) أمرٌ بسيط:

  1. إنشاء الصنف الفرعي لمدير العرض ViewManager
  2. كتابة التابع createViewInstance
  3. توفير الوصول إلى توابع setter لضبط خاصيّات العرض باستخدام التوصيف ‎‎@ReactProp‎‎ (‎‎أو ‎‎@ReactPropGroup‎‎)
  4. تسجيل المدير في createViewManagers الخاص بحزمة التطبيقات.
  5. كتابة وحدة JavaScript

1. إنشاء الصنف الفرعي لمدير العرض ViewManager

في هذا المثال، سننشئ صنف مدير العرض ReactImageManager الذي يوسِّع SimpleViewManager من النوع ReactImageView. النوع ReactImageView هو نوع الكائن الذي يديره المدير، وسيكون هذا هو العرض الأصيل المخصص. يُستخدَم الاسم المعاد من طرف getName لإحالة (to reference) نوع العرض الأصيل من JavaScript.

...

public class ReactImageManager extends SimpleViewManager<ReactImageView> {

  public static final String REACT_CLASS = "RCTImageView";

  @Override
  public String getName() {
    return REACT_CLASS;
  }

2. كتابة التابع createViewInstance

تنشأ العروض في تابع createViewInstance، يجب أن يُهيِّئَ العرض نفسه في حالته الافتراضية (default state)، وستُعيَّن الخاصيات عبر استدعاء updateView لاحقًا.

@Override
  public ReactImageView createViewInstance(ThemedReactContext context) {
    return new ReactImageView(context, Fresco.newDraweeControllerBuilder(), mCallerContext);
  }

3. توفير الوصول إلى توابع setter لضبط خاصيّات العرض باستخدام التوصيف ‎‎@ReactProp‎‎ (‎‎أو ‎@ReactPropGroup‎‎)

يجب توفير الوصول للخاصيّات التي يجب أن تنعكس في JavaScript كتابع setter موصوف (annotated) باستخدام التوصيف ‎‎@ReactProp‎‎ (‎‎أو ‎@ReactPropGroup‎‎). يجب أن يأخذ تابع Setter العرض المراد تحديثه (من نوع العرض الحالي) كأوّل معامل، وقيمة الخاصية كمعامل ثانٍ. يجب إعلان Setter كتابع void ويجب أن يكون عامًّا (public). يُحدّد نوع الخاصية المرسلة إلى JavaScript تلقائيًا استنادًا إلى نوع المعامل القيمة الخاص بتابع setter. أنواع القيم المدعومة حاليًا هي: ‎‎boolean, int, float, double, String, Boolean, Integer, ReadableArray, ReadableMap‎‎.

يحتاج التوصيف ‎‎@ReactProp‎‎ معاملًا إلزاميًّا واحدًا يُسمّى name من النوع String يمثّل اسم التوصيف. يُستخدَم الاسم المعيَّن للتوصيف ‎‎@ReactProp‎‎ المرتبط بالتابع setter للإشارة إلى الخاصية في جانب JavaScript.

باستثناء name، يقبل التوصيف ‎‎@ReactProp‎‎ المعاملات الاختيارية التالية: defaultBoolean، وdefaultInt، وdefaultFloat. يجب أن تكون هذه المعاملات من النوع البدائي المقابل (بالتالي boolean، وint، وfloat) وستمرَّر القيمة المعطاة إلى تابع setter في حالة إزالة الخاصية التي يشير إليها تابع setter من المكون. لاحظ أن القيم "الافتراضية ("default")" موفَّرة فقط للأنواع البدائية، إذا كان تابع setter نوعاً معقداً، ستوفَّر القيمة null كقيمة افتراضية في حالة إزالة الخاصية المطابقة.

متطلبات إعلان (declaration) تابع Setter للتوابع الموصوفة بالتوصيف ‎‎@ReactPropGroup‎‎ مختلفة عن متطلبات ‎‎@ReactProp‎‎، يرجى الرجوع إلى توثيقات صنف التوصيف ‎‎@ReactPropGroup‎‎ للمزيد من المعلومات حول هذا الموضوع.

ملاحظة مهمّة! في ReactJS تحديث قيمة الخاصية سيؤدي إلى استدعاء تابع setter. لاحظ أن إحدى الطرق لتحديث المكون هي بإزالة الخاصيات التي عُيِّنت من قبل. في هذه الحالة، سيستدعى تابع setter كذلك لإشعار مدير العرض بأن الخاصية قد تغيرت. في هذه الحالة، ستوفَّر القيمة "الافتراضية" (للأنواع البدائية، يُمكن تحديد القيم "الافتراضيّة" باستخدام defaultBoolean، وdefaultFloat، إلخ. والتي هي معاملات التوصيف ‎‎@ReactProp‎‎، للأنواع المعقَّدة، سيُستدعى setter مع القيمة null).

  @ReactProp(name = "src")
  public void setSrc(ReactImageView view, @Nullable ReadableArray sources) {
    view.setSource(sources);
  }

  @ReactProp(name = "borderRadius", defaultFloat = 0f)
  public void setBorderRadius(ReactImageView view, float borderRadius) {
    view.setBorderRadius(borderRadius);
  }

  @ReactProp(name = ViewProps.RESIZE_MODE)
  public void setResizeMode(ReactImageView view, @Nullable String resizeMode) {
    view.setScaleType(ImageResizeMode.toScaleType(resizeMode));
  }

4. تسجيل مدير العرض ViewManager

الخطوة الأخيرة في Java هي تسجيل مدير العرض للتطبيق، وهذا يحدث بطريقة مشابهة للوحدات الأصيلة عبر دالة عضو حزمة (package member function) التطبيق createViewManagers.

  @Override
  public List<ViewManager> createViewManagers(
                            ReactApplicationContext reactContext) {
    return Arrays.<ViewManager>asList(
      new ReactImageManager()
    );
  }

5. إنشاء وحدة JavaScript

الخطوة الأخيرة هي إنشاء وحدة JavaScript تحدد طبقة الواجهة (interface layer) بين Java و JavaScript لمستخدمي عرضك الجديد. يوصى بتوثيق واجهة المكون في هذه الوحدة (باستخدام Flow أو TypeScript أو التعليقات مثلًا).

// ImageView.js

import {requireNativeComponent} from 'react-native';

/**
 * Composes `View`.
 *
 * - src: string
 * - borderRadius: number
 * - resizeMode: 'cover' | 'contain' | 'stretch'
 */
module.exports = requireNativeComponent('RCTImageView');

تأخذ الدالة requireNativeComponent اسم العرض الأصيل. لاحظ أنه إذا احتاج مكونك إلى القيام بأي شيء أعقد (مثل معالجة الأحداث المخصصة)، فعليك تغليف المكون الأصيل في مكون React آخر. هذا موضح في مثال MyCustomView أدناه.

الأحداث (Events)

نعرف الآن كيف نوفر الوصول لمكونات العرض الأصيل التي يمكننا التحكم فيها بسهولة من JavaScript، لكن كيف نتعامل مع الأحداث من المستخدم، مثل القرص للتكبير أو للتصغير أو المسح (panning). عند حدوثِ حدثٍ أصيل، يجب أن تصدر الشيفرة الأصيلة حدثًا للعرض في JavaScript، مع ربط العرضين بالقيمة المُعادة من تابع ‎‎getId()‎‎.

class MyCustomView extends View {
   ...
   public void onReceiveNativeEvent() {
      WritableMap event = Arguments.createMap();
      event.putString("message", "MyMessage");
      ReactContext reactContext = (ReactContext)getContext();
      reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(
          getId(),
          "topChange",
          event);
    }
}

لربط اسم حدث topChange إلى خاصية دالة رد النداء onChange في JavaScript، سجِّله عن طريق تجاوز تابع ‎‎getExportedCustomBubblingEventTypeConstants‎‎ في مدير العرض ViewManager الخاص بك:

public class ReactImageManager extends SimpleViewManager<MyCustomView> {
    ...
    public Map getExportedCustomBubblingEventTypeConstants() {
        return MapBuilder.builder()
            .put(
                "topChange",
                MapBuilder.of(
                    "phasedRegistrationNames",
                    MapBuilder.of("bubbled", "onChange")))
                    .build();
    }
}

تُستدعَى دالة رد النداء هذه مع الحدث الخام (raw event)، والذي نقوم بمعالجته عادةً في المكون المُغلِّف (wrapper component) لإنشاء واجهة برمجة تطبيقات أبسط:

// MyCustomView.js

class MyCustomView extends React.Component {
  constructor(props) {
    super(props);
    this._onChange = this._onChange.bind(this);
  }
  _onChange(event: Event) {
    if (!this.props.onChangeMessage) {
      return;
    }
    this.props.onChangeMessage(event.nativeEvent.message);
  }
  render() {
    return <RCTMyCustomView {...this.props} onChange={this._onChange} />;
  }
}
MyCustomView.propTypes = {
  /**
   * دالة رد نداء تُستدعى باستمرار عندما يتفاعل المستخدم مع المكوّن
   */
  onChangeMessage: PropTypes.func,
  ...
};

var RCTMyCustomView = requireNativeComponent(`RCTMyCustomView`);

مصادر