المكونات الإضافية لواجهة مستخدم السيارة

استخدِم المكوّنات الإضافية في مكتبة واجهة المستخدم في السيارة لإنشاء عمليات تنفيذ كاملة لتعديلات المكوّنات في مكتبة واجهة المستخدم في السيارة بدلاً من استخدام تراكب موارد وقت التشغيل (RRO). تتيح لك عمليات التثبيت من مصدر غير معروف تغيير موارد XML فقط لمكوّنات مكتبة واجهة مستخدم السيارة، ما يحدّ من مدى التخصيص الذي يمكنك إجراؤه.

إنشاء مكوّن إضافي

المكوّن الإضافي لمكتبة واجهة مستخدم السيارة هو حزمة APK تحتوي على فئات تنفِّذ مجموعة من واجهات برمجة التطبيقات للمكوّنات الإضافية. يمكن تجميع واجهات برمجة التطبيقات الخاصة بالمكونات الإضافية في ملف مكوّن إضافي كمكتبة ثابتة.

اطّلِع على أمثلة في Soong وGradle:

سونغ

إليك مثال على Soong:

android_app {
    name: "my-plugin",

    min_sdk_version: "28",
    target_sdk_version: "30",
    aaptflags: ["--shared-lib"],
    sdk_version: "current",

    manifest: "src/main/AndroidManifest.xml",
    srcs: ["src/main/java/**/*.java"],
    resource_dirs: ["src/main/res"],
    static_libs: [
        "car-ui-lib-oem-apis",
    ],
    // Disable optimization is mandatory to prevent R.java class from being
    // stripped out
    optimize: {
        enabled: false,
    },

    certificate: ":my-plugin-certificate",
}

Gradle

اطّلِع على ملف build.gradle هذا:

apply plugin: 'com.android.application'

android {
  compileSdkVersion 30

  defaultConfig {
    minSdkVersion 28
    targetSdkVersion 30
  }

  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }

  signingConfigs {
    debug {
      storeFile file('chassis_upload_key.jks')
      storePassword 'chassis'
      keyAlias 'chassis'
      keyPassword 'chassis'
    }
  }
}

dependencies {
  implementation project(':oem-apis')
  // Or use the following if you'd like to use the maven artifact
  // implementation 'com.android.car.ui:car-ui-lib-plugin-apis:1.0.0'
}

Settings.gradle:

// You can remove the ':oem-apis' if you're using the maven artifact.
include ':oem-apis'
project(':oem-apis').projectDir = new File('./path/to/oem-apis')
include ':my-plugin'
project(':my-plugin').projectDir = new File('./my-plugin')

يجب أن يتضمّن المكوّن الإضافي موفّر محتوى مُعلَنًا عنه في البيان الذي يتضمّن السمات التالية:

  android:authorities="com.android.car.ui.plugin"
  android:enabled="true"
  android:exported="true"

android:authorities="com.android.car.ui.plugin" يجعل المكوّن الإضافي قابلاً للاكتشاف في مكتبة واجهة مستخدم السيارة. يجب تصدير موفِّر الخدمة حتى يمكن الاستعلام عنه أثناء وقت التشغيل. بالإضافة إلى ذلك، في حال ضبط السمة enabled على false، سيتم استخدام التنفيذ التلقائي بدلاً من تنفيذ المكوّن الإضافي. ولا يُشترط أن تكون فئة مقدّم المحتوى متوفرة. في هذه الحالة، احرص على إضافة tools:ignore="MissingClass" إلى تعريف الموفِّر. اطّلِع على نموذج إدخال البيان أدناه:

    <application>
        <provider
            android:name="com.android.car.ui.plugin.PluginNameProvider"
            android:authorities="com.android.car.ui.plugin"
            android:enabled="false"
            android:exported="true"
            tools:ignore="MissingClass"/>
    </application>

أخيرًا، كإجراء أمني، وقِّع على تطبيقك.

الإضافات بصفتها مكتبة مشتركة

على عكس مكتبات Android الثابتة التي يتم تجميعها مباشرةً في التطبيقات، تتم تجميع مكتبات Android المشتركة في حزمة APK مستقلة تتم الإشارة إليها من قِبل التطبيقات الأخرى في وقت التشغيل.

بالنسبة إلى المكوّنات الإضافية التي يتم تنفيذها كمكتبة مشترَكة على Android، تتم إضافة صفوفها تلقائيًا إلى أداة تحميل الصفوف المشترَكة بين التطبيقات. عندما يحدِّد تطبيق يستخدم مكتبة واجهة مستخدِم السيارة تبعية وقت التشغيل على المكتبة المشتركة للمكوّن الإضافي، يمكن لتحميل الفئات في التطبيق الوصول إلى فئات المكتبة المشتركة للمكوّن الإضافي. يمكن أن تؤثر المكونات الإضافية التي تم تنفيذها كتطبيقات Android عادية (وليس مكتبة مشتركة) بشكل سلبي في مدة بدء التطبيق من الصفر.

تنفيذ المكتبات المشتركة وإنشاؤها

يشبه تطوير التطبيقات باستخدام مكتبات Android المشتركة إلى حد كبير تطوير التطبيقات العادية لنظام التشغيل Android، مع بعض الاختلافات الرئيسية.

  • استخدِم علامة library ضمن علامة application مع اسم حزمة الإضافة في بيان تطبيق الإضافة:
    <application>
        <library android:name="com.chassis.car.ui.plugin" />
        ...
    </application>
  • اضبط قاعدة الإنشاء android_app في Soong (Android.bp) باستخدام علامة AAPT shared-lib، والتي تُستخدَم لإنشاء مكتبة مشترَكة:
android_app {
  ...
  aaptflags: ["--shared-lib"],
  ...
}

العناصر التابعة للمكتبات المشتركة

لكل تطبيق على النظام يستخدم مكتبة واجهة مستخدم السيارة، يجب تضمين العلامة uses-library في ملف بيان التطبيق ضمن العلامة application مع اسم حزمة المكوّن الإضافي:

<manifest>
  <application
      android:name=".MyApp"
      ...>
    <uses-library android:name="com.chassis.car.ui.plugin" android:required="false"/>
    ...
  </application>
</manifest>

تثبيت مكوّن إضافي

يجب تثبيت المكوّنات الإضافية مسبقًا في قسم النظام من خلال تضمين الوحدة في PRODUCT_PACKAGES. يمكن تحديث الحزمة المثبَّتة مسبقًا بالطريقة نفسها التي يتم بها تحديث أي تطبيق آخر مثبَّت.

إذا كنت بصدد تحديث مكوّن إضافي حالي على النظام، سيتم تلقائيًا إغلاق أي تطبيقات تستخدم هذا المكوّن الإضافي. وبعد أن يعيد المستخدم فتح الملف، تظهر له التغييرات المعدَّلة. إذا لم يكن التطبيق قيد التشغيل، سيحتوي على الإضافة المعدَّلة عند تشغيله في المرة التالية.

عند تثبيت مكوّن إضافي باستخدام Android Studio، هناك بعض النقاط العميقة التي يجب أخذها في الاعتبار. في وقت كتابة هذه المقالة، هناك خطأ في عملية تثبيت تطبيق Android Studio يؤدي إلى عدم سريان تحديثات المكوّن الإضافي. يمكن حلّ هذه المشكلة من خلال اختيار الخيار التثبيت دائمًا باستخدام مدير الحِزم (يؤدي ذلك إلى إيقاف تحسينات النشر على Android 11 والإصدارات الأحدث) في إعدادات إنشاء المكوّن الإضافي.

بالإضافة إلى ذلك، عند تثبيت المكوّن الإضافي، يُبلغ Android Studio عن خطأ مفاده أنّه لا يمكنه العثور على نشاط رئيسي لتشغيله. من المتوقّع حدوث ذلك، لأنّ المكوّن الإضافي ليس لديه أي أنشطة (باستثناء الهدف الفارغ المستخدَم لحلّ هدف). لتجنُّب الخطأ، غيِّر خيار التشغيل إلى بدون في إعدادات الإصدار.

إعدادات المكوّن الإضافي في &quot;استوديو Android&quot; الشكل 1. إعدادات المكوّن الإضافي في "استوديو Android"

المكوّن الإضافي للخادم الوكيل

يتطلّب تخصيص التطبيقات باستخدام مكتبة واجهة المستخدم في السيارة استخدام إذن وصول مُبرمَج إلى نظام التشغيل (RRO) يستهدف كل تطبيق محدّد ليتم تعديله، بما في ذلك عندما تكون عمليات التخصيص متطابقة في جميع التطبيقات. وهذا يعني أنّه يجب تقديم طلب للحصول على إذن بالوصول إلى الموارد لكل تطبيق. الاطّلاع على التطبيقات التي تستخدم مكتبة واجهة المستخدم في السيارة

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

لا يمثّل المكوّن الإضافي للوكيل سوى مثال ونقطة بداية لإجراء التخصيص باستخدام مكوّن إضافي. بالنسبة إلى التخصيص الذي يتجاوز عمليات التثبيت من مصدر غير معروف، يمكن تنفيذ مجموعة فرعية من مكونات الإضافة واستخدام الإضافة الوكيلة للباقي، أو تنفيذ جميع مكونات الإضافة بالكامل من البداية.

على الرغم من أنّ المكوّن الإضافي للوكيل يقدّم نقطة واحدة لتخصيص ميزة "إعادة توجيه البيانات في التطبيقات" للتطبيقات، ستظل التطبيقات التي تتوقف عن استخدام المكوّن الإضافي تتطلّب ميزة "إعادة توجيه البيانات في التطبيقات" التي تستهدف التطبيق نفسه مباشرةً.

تنفيذ واجهات برمجة التطبيقات الخاصة بالمكوّنات الإضافية

نقطة الدخول الرئيسية إلى المكوّن الإضافي هي فئة com.android.car.ui.plugin.PluginVersionProviderImpl. يجب أن تتضمّن كل المكونات الإضافية فئة تحمل هذا الاسم الدقيق واسم الحزمة. يجب أن تتضمّن هذه الفئة constructor تلقائيًا وأن تنفّذ واجهة PluginVersionProviderOEMV1.

يجب أن تعمل مكوّنات CarUi الإضافية مع التطبيقات الأقدم أو الأحدث من المكوّن الإضافي. لتسهيل ذلك، يتمّ تحديد إصدارات جميع واجهات برمجة التطبيقات الخاصة بالمكونات الإضافية باستخدام V# في نهاية اسم الفئة. إذا تم إصدار إصدار جديد من مكتبة واجهة المستخدم في السيارة يتضمّن ميزات جديدة، تكون هذه الميزات جزءًا من الإصدار V2 من المكوّن. تبذل مكتبة واجهة المستخدم في السيارة قصارى جهدها لجعل الميزات الجديدة تعمل في نطاق مكوّن إضافي قديم. على سبيل المثال، من خلال تحويل نوع جديد من الأزرار في شريط الأدوات إلى MenuItems.

ومع ذلك، لا يمكن لتطبيق يستخدم إصدارًا قديمًا من مكتبة واجهة المستخدم في السيارة استخدام مكوّن تكميلي جديد مكتوب باستخدام واجهات برمجة تطبيقات أحدث. لحلّ هذه المشكلة، نسمح للمكوّنات الإضافية بأن تعرِض تنفيذات مختلفة لنفسها استنادًا إلى إصدار OEM API الذي تتوافق معه التطبيقات.

يحتوي PluginVersionProviderOEMV1 على طريقة واحدة:

Object getPluginFactory(int maxVersion, Context context, String packageName);

تعرض هذه الطريقة عنصرًا ينفِّذ أحدث إصدار من PluginFactoryOEMV# يتيحّه المكوّن الإضافي، مع أنّه لا يزال أقل من maxVersion أو مساوياً له. إذا لم يتضمّن المكوّن الإضافي تنفيذًا لملف برمجي PluginFactory قديم، قد يعرض القيمة null، وفي هذه الحالة، يتم استخدام التنفيذ المرتبط statically- لمكوّنات CarUi.

للحفاظ على التوافق مع الإصدارات القديمة من التطبيقات التي تم تجميعها باستخدام الإصدارات القديمة من مكتبة Car Ui الثابتة، ننصحك بتوفّر الإصدارين maxVersion 2 و5 والإصدارات الأحدث من داخل تنفيذ المكوّن الإضافي لPluginVersionProvider. الإصدارات 1 و3 و4 غير متوافقة. لمزيد من المعلومات، يُرجى الاطّلاع على PluginVersionProviderImpl.

PluginFactory هي الواجهة التي تنشئ جميع مكونات CarUi الأخرى. ويحدّد أيضًا إصدار واجهاتها التي يجب استخدامها. إذا كان المكوّن الإضافي لا يسعى إلى تنفيذ أيٍّ من هذه المكوّنات، قد يعرضnull في وظيفة الإنشاء (باستثناء شريط الأدوات الذي يحتوي علىcustomizesBaseLayout() منفصلة).

يحدّد pluginFactory إصدارات مكونات CarUi التي يمكن استخدامها معًا. على سبيل المثال، لن يكون هناك pluginFactory يمكنه إنشاء الإصدار 100 من Toolbar والإصدار 1 من RecyclerView، لأنّه لن يكون هناك ضمان تقريبًا بأنّ مجموعة كبيرة من إصدارات المكوّنات لن تعمل معًا. لاستخدام الإصدار 100 من شريط الأدوات، من المتوقّع من المطوّرين أن يوفّروا تنفيذًا لإصدار من pluginFactory ينشئ الإصدار 100 من شريط الأدوات، ما يحدّ من خيارات إصدارات المكونات الأخرى التي يمكن إنشاؤها. قد لا تكون إصدارات المكوّنات الأخرى متطابقة، على سبيل المثال، يمكن أن ينشئ pluginFactoryOEMV100 ToolbarControllerOEMV100 وRecyclerViewOEMV70.

شريط الأدوات

التنسيق الأساسي

يرتبط شريط الأدوات بـ "التنسيق الأساسي" ارتباطًا وثيقًا، لذا تُسمى الدالة التي تنشئ شريط الأدوات installBaseLayoutAround. التنسيق الأساسي: هو مفهوم يسمح بوضع شريط الأدوات في أي مكان حول محتوى التطبيق، للسماح بعرض شريط الأدوات في أعلى/أسفل التطبيق، أو عموديًا على الجانبين، أو حتى شريط أدوات دائري يحيط بالتطبيق بأكمله. ويتم تنفيذ ذلك من خلال تمرير عرض إلى installBaseLayoutAround لكي يتم لف شريط الأدوات/التنسيق الأساسي حوله.

يجب أن يأخذ المكوّن الإضافي طريقة العرض المقدَّمة ويفصلها عن العنصر الرئيسي، ثم يضخِّم تصميم المكوّن الإضافي في الفهرس نفسه للعنصر الرئيسي وباستخدام LayoutParams نفسه المستخدَم في طريقة العرض التي تم فصلها للتو، ثم يعيد إرفاق طريقة العرض في مكان ما داخل التصميم الذي تم تضخيمه للتو. سيتضمّن التنسيق الموسّع شريط الأدوات، إذا طلب التطبيق ذلك.

يمكن للتطبيق طلب تنسيق أساسي بدون شريط أدوات. إذا حدث ذلك، من المفترض أن تعرِض الدالة installBaseLayoutAround قيمة فارغة. في معظم الإضافات، هذا هو كل ما يجب تنفيذه، ولكن إذا أراد مؤلف الإضافة تطبيق زخرفة مثلاً حول حافة التطبيق، يمكنه إجراء ذلك باستخدام تنسيق أساسي. تكون هذه الزخارف مفيدة بشكل خاص للأجهزة التي تحتوي على شاشات غير مستطيلة، لأنّها يمكنها دفع التطبيق إلى مساحة مستطيلة وإضافة انتقالات واضحة إلى المساحة غير المستطيلة.

يتم أيضًا تمرير Consumer<InsetsOEMV1> إلى installBaseLayoutAround. يمكن استخدام استهلاك البيانات هذا للتواصل مع التطبيق وإبلاغه بأنّ المكوّن الإضافي يغطي محتوى التطبيق بشكلٍ جزئي (باستخدام شريط الأدوات أو غير ذلك). سيعرف التطبيق بعد ذلك أنّه يجب مواصلة الرسم في هذه المساحة، ولكن يجب إبقاء أي مكوّنات مهمة يمكن للمستخدم التفاعل معها خارجها. يتم استخدام هذا التأثير في التصميم المرجعي، لجعل toolbar (شريط الأدوات) شبه شفاف، ولعرض القوائم تحته. في حال عدم تنفيذ هذه الميزة، سيتم تثبيت العنصر الأول في القائمة أسفل شريط الأدوات ولن يكون قابلاً للنقر. إذا لم يكن هذا التأثير مطلوبًا، يمكن للمكوّن الإضافي تجاهل ملف تعريف الارتباط consumer.

تمرير المحتوى أسفل شريط الأدوات الشكل 2: تمرير المحتوى أسفل شريط الأدوات

من منظور التطبيق، عندما يُرسِل المكوّن الإضافي عناصر عرض جديدة، سيتلقّاها من أيّ أنشطة أو أجزاء تنفِّذ InsetsChangedListener. إذا كان النشاط أو التجزئة لا ينفِّذ InsetsChangedListener، ستتولى مكتبة Car Ui معالجة الأجزاء المضمّنة تلقائيًا من خلال تطبيق الأجزاء المضمّنة كحشو على Activity أو FragmentActivity التي تحتوي على التجزئة. لا تطبِّق المكتبة المكوّنات المضمّنة تلقائيًا على الأجزاء. في ما يلي نموذج مقتطف من عملية تنفيذ تطبّق الأجزاء المُدمجة كحشو على RecyclerView في التطبيق:

public class MainActivity extends Activity implements InsetsChangedListener {
  @Override
  public void onCarUiInsetsChanged(Insets insets) {
    CarUiRecyclerView rv = requireViewById(R.id.recyclerview);
    rv.setPadding(insets.getLeft(), insets.getTop(),
                  insets.getRight(), insets.getBottom());
  }
}

أخيرًا، يتم منح المكوّن الإضافي تلميحًا fullscreen، والذي يُستخدَم للإشارة إلى ما إذا كان المشهد الذي يجب تغليفه يشغل التطبيق بأكمله أو قسمًا صغيرًا فقط. يمكن استخدام هذا الخيار لتجنُّب تطبيق بعض الزخارف على الحافة التي لا تكون منطقية إلا إذا كانت تظهر على طول حافة الشاشة بأكملها. أحد نماذج التطبيقات التي تستخدم تصاميم أساسية غير ملء الشاشة هو "الإعدادات"، حيث يحتوي كل قسم من تخطيط الشاشة المزدوجة على شريط أدوات خاص به.

بما أنّه من المتوقّع أن تعرِض installBaseLayoutAround قيمة فارغة عندما يكون toolbarEnabled يساوي false، لكي يشير المكوّن الإضافي إلى أنّه ليس لديه رغبة في تخصيص التنسيق الأساسي، يجب أن تعرِض false من customizesBaseLayout.

يجب أن يحتوي تنسيق القاعدة على FocusParkingView وFocusArea لكي تتوافق مع عناصر التحكّم الدوّارة بشكل كامل. يمكن حذف هذه المشاهد على الأجهزة التي لا تتيح العرض على الشاشة الدوّارة. يتم تنفيذ FocusParkingView/FocusAreas في مكتبة CarUi static، لذا يتم استخدام setRotaryFactories لتوفير مصانع ل إنشاء طرق العرض من السياقات.

يجب أن تكون السياقات المستخدَمة لإنشاء "طرق العرض في وضع التركيز" هي السياق المصدر، وليس سياق الإضافة. يجب أن يكون FocusParkingView أقرب ما يمكن إلى العرض الأول في الشجرة، لأنّه هو العنصر الذي يتم التركيز عليه عندما لا ينبغي أن يكون هناك تركيز مرئي للمستخدم. يجب أن يلف الرمز FocusArea شريط الأدوات في التنسيق الأساسي للإشارة إلى أنّه منطقة حثّ دوّارة. إذا لم يكن FocusArea متاحًا، لن يتمكّن المستخدم من الانتقال إلى أي أزرار في شريط الأدوات باستخدام المحرِّك الدوار.

وحدة التحكّم في شريط الأدوات

يجب أن يكون من السهل تنفيذ ToolbarController الفعلي الذي يتم إرجاعه مقارنةً بالتنسيق الأساسي. ووظيفته هي أخذ المعلومات التي يتم تمريرها إلى ملفَّي برمجة تطبيقات ‎set وعرضها في التنسيق الأساسي. اطّلِع على Javadoc للحصول على معلومات عن معظم الطرق. في ما يلي بعض الطرق الأكثر تعقيدًا.

يُستخدَم الرمز getImeSearchInterface لعرض نتائج البحث في نافذة IME (لوحة المفاتيح). يمكن أن يكون ذلك مفيدًا لعرض نتائج البحث أو إضافة تأثيرات متحرّكة إليها بجانب لوحة المفاتيح، على سبيل المثال إذا كانت لوحة المفاتيح تشغل نصف الشاشة فقط. يتم تنفيذ معظم الوظائف في مكتبة CarUi الثابتة، وتوفر واجهة البحث في المكوّن الإضافي طرقًا للمكتبة الثابتة للحصول على TextView وonPrivateIMECommand. ولتفعيل هذه الميزة، يجب أن يستخدم المكوّن الإضافي فئة فرعية من TextView تلغي onPrivateIMECommand وتُمرِّر الطلب إلى المستمع المقدَّم باعتبارهTextView لشريط البحث.

setMenuItems يعرض ببساطة عناصر القائمة على الشاشة، ولكن سيتم استدعاؤه بمعدل مدهش. بما أنّ واجهة برمجة التطبيقات الخاصة بالمكوّن الإضافي لعناصر القائمة غير قابلة للتغيير، سيتم إجراء طلب setMenuItems جديد بالكامل كلما تم تغيير ملف ملف setMenuItems. وقد يحدث ذلك لسبب بسيط مثل نقر المستخدم على عنصر MenuItem للتبديل، ما يؤدي بدوره إلى تبديل التبديل. لأسباب تتعلّق بالأداء والرسوم المتحركة، ننصح بحساب الفرق بين القائمتَين القديمتَين MenuItems، وتعديل العروض التي تغيّرت فقط. توفّر عناصر القائمة حقل key يمكن أن يساعد في ذلك، لأنّ المفتاح يجب أن يكون هو نفسه في جميع طلبات البيانات المختلفة إلى setMenuItems لعنصر القائمة نفسه.

AppStyledView

AppStyledView هي حاوية لعرض لم يتم تخصيصه على الإطلاق. ويمكن استخدامها لتوفير حدود حول هذا العرض تجعله يبرز عن بقية أجزاء التطبيق، وتشير إلى المستخدم بأنّ هذا نوع مختلف من واجهة المستخدم. يتم عرض العرض الذي يتم تغليفه بواسطة AppStyledView في setContent. يمكن أن يتضمّن الرمز AppStyledView أيضًا زر الرجوع أو الإغلاق على النحو الذي يطلبه التطبيق.

لا تُدرج AppStyledView مشاهدها على الفور في التسلسل الهرمي للمشاهد مثلما تفعل installBaseLayoutAround، بل تُعيد عرضها إلى المكتبة الثابتة من خلال getView، التي تُجري عملية الإدراج بعد ذلك. يمكن أيضًا التحكّم في موضع AppStyledView وحجمه من خلال تنفيذ getDialogWindowLayoutParam.

السياقات

يجب أن يكون المكوّن الإضافي حذرًا عند استخدام السياقات، لأنّ هناك سياقَي المكوّن الإضافي و "المصدر". يتم تقديم سياق المكوّن الإضافي كوسيطة لسمة getPluginFactory، وهو السياق الوحيد الذي يحتوي على موارد المكوّن الإضافي. وهذا يعني أنّه السياق الوحيد الذي يمكن استخدامه ل تضخيم التنسيقات في المكوّن الإضافي.

ومع ذلك، قد لا يكون سياق المكوّن الإضافي قد تم ضبط الإعداد الصحيح عليه. للحصول على الإعداد الصحيح، نوفّر سياقات المصدر في الطرق التي تُنشئ المكوّنات. يكون سياق المصدر عادةً نشاطًا، ولكن قد يكون في بعض الحالات أيضًا خدمة أو مكوّن Android آخر. لاستخدام الإعدادات من ملف تعريف المصدر مع الموارد من ملف تعريف المكوّن الإضافي، يجب إنشاء ملف تعريف جديد باستخدام createConfigurationContext. في حال عدم استخدام الإعداد الصحيح، سيحدث انتهاك للوضع الصارم في Android، وقد لا تمتلك المشاهدات الموسّعة السمات الصحيحة.

Context layoutInflationContext = pluginContext.createConfigurationContext(
        sourceContext.getResources().getConfiguration());

تغييرات الوضع

يمكن لبعض الإضافات أن تتيح أوضاعًا متعددة لمكوّناتها، مثل وضع الرياضة أو وضع توفير الطاقة اللذَين يختلفان بشكل مرئي. لا تتوفّر ميزة مدمجة لاستخدام هذه الوظيفة في CarUi، ولكن لا شيء يمنع المكوّن الإضافي من تنفيذها داخليًا بالكامل. يمكن للمكوّن الإضافي مراقبة أيّ شروط يريدها لمعرفة وقت التبديل بين الأوضاع، مثل الاستماع إلى البثّ. لا يمكن للإضافة أن تؤدي إلى تغيير في الإعدادات لتغيير الأوضاع، ولكن لا يُنصح بالاعتماد على تغييرات الإعدادات على أي حال، لأنّ تعديل مظهر كل مكوّن يدويًا هو عملية أسهل للمستخدم ويسمح أيضًا بعمليات انتقال لا يمكن إجراؤها باستخدام تغييرات الإعدادات.

Jetpack Compose

يمكن تنفيذ المكوّنات الإضافية باستخدام Jetpack Compose، ولكن هذه ميزة في مرحلة ألفا ولا يُفترض أن تكون مستقرة.

يمكن للمكوّنات الإضافية استخدام رمز ComposeView لإنشاء سطح عرض متوافق مع ميزة "الإنشاء" لعرض المحتوى عليه. سيكون هذا العنصر ComposeView هو ما يتم إرجاعه إلى التطبيق من طريقة getView في المكوّنات.

تتمثل إحدى المشاكل الرئيسية في استخدام ComposeView في أنّه يضبط العلامات على طريقة العرض الجذر في التنسيق من أجل تخزين المتغيّرات الشاملة التي تتم مشاركتها على مستوى ComposeViews المختلفة في التسلسل الهرمي. بما أنّ أرقام تعريف الموارد للمكوّن الإضافي لا يتم تحديد نطاقها بشكل منفصل عن أرقام تعريف الموارد للتطبيق، قد يؤدي ذلك إلى حدوث تعارضات عندما يحدّد كل من التطبيق والمكوّن الإضافي علامات في العرض نفسه. في ما يلي ملف شخصي مخصّص ComposeViewWithLifecycle ينقل هذه المتغيّرات الشاملة إلى ComposeView. ولا يُفترض أن تكون هذه الإصدارات مستقرة.

ComposeViewWithLifecycle:

class ComposeViewWithLifecycle @JvmOverloads constructor(
  context: Context,
  attrs: AttributeSet? = null,
  defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr),
    LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner {

  private val lifeCycle = LifecycleRegistry(this)
  private val modelStore = ViewModelStore()
  private val savedStateRegistryController = SavedStateRegistryController.create(this)
  private var composeView: ComposeView? = null
  private var content = @Composable {}

  init {
    ViewTreeLifecycleOwner.set(this, this)
    ViewTreeViewModelStoreOwner.set(this, this)
    ViewTreeSavedStateRegistryOwner.set(this, this)
    compositionContext = createCompositionContext()
  }

  fun setContent(content: @Composable () -> Unit) {
    this.content = content
    composeView?.setContent(content)
  }

  override fun getLifecycle(): Lifecycle {
    return lifeCycle
  }

  override fun getViewModelStore(): ViewModelStore {
    return modelStore
  }

  override fun getSavedStateRegistry(): SavedStateRegistry {
    return savedStateRegistryController.savedStateRegistry
  }

  override fun onAttachedToWindow() {
    super.onAttachedToWindow()
    savedStateRegistryController.performRestore(Bundle())
    lifeCycle.currentState = Lifecycle.State.RESUMED
    composeView = ComposeView(context)
    composeView?.setContent(content)
    addView(composeView, LayoutParams(
      LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
  }

  override fun onDetachedFromWindow() {
    super.onDetachedFromWindow()
    lifeCycle.currentState = Lifecycle.State.DESTROYED
    modelStore.clear()
    removeAllViews()
    composeView = null
  }

  // Exact copy of View.createCompositionContext() in androidx's WindowRecomposer.android.kt
  private fun createCompositionContext(): CompositionContext {
    val currentThreadContext = AndroidUiDispatcher.CurrentThread
    val pausableClock = currentThreadContext[MonotonicFrameClock]?.let {
      PausableMonotonicFrameClock(it).apply { pause() }
    }
    val contextWithClock = currentThreadContext + (pausableClock ?: EmptyCoroutineContext)
    val recomposer = Recomposer(contextWithClock)
    val runRecomposeScope = CoroutineScope(contextWithClock)
    val viewTreeLifecycleOwner = checkNotNull(ViewTreeLifecycleOwner.get(this)) {
      "ViewTreeLifecycleOwner not found from $this"
    }
    viewTreeLifecycleOwner.lifecycle.addObserver(
      LifecycleEventObserver { _, event ->
        @Suppress("NON_EXHAUSTIVE_WHEN")
        when (event) {
          Lifecycle.Event.ON_CREATE ->
            // Undispatched launch since we've configured this scope
            // to be on the UI thread
            runRecomposeScope.launch(start = CoroutineStart.UNDISPATCHED) {
              recomposer.runRecomposeAndApplyChanges()
            }
          Lifecycle.Event.ON_START -> pausableClock?.resume()
          Lifecycle.Event.ON_STOP -> pausableClock?.pause()
          Lifecycle.Event.ON_DESTROY -> {
            recomposer.cancel()
          }
        }
      }
    )
    return recomposer
  }

//  TODO: ComposeViewWithLifecycle should handle saving state and other lifecycle things
//  override fun onSaveInstanceState(): Parcelable? {
//    val superState = super.onSaveInstanceState()
//    val bundle = Bundle()
//    savedStateRegistryController.performSave(bundle)
//  }
}