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

استخدم المكونات الإضافية لمكتبة Car UI لإنشاء تطبيقات كاملة لتخصيصات المكونات في مكتبة Car UI بدلاً من استخدام تراكبات موارد وقت التشغيل (RROs). تمكنك عمليات RROs من تغيير موارد XML الخاصة بمكونات مكتبة Car UI فقط، مما يحد من مدى ما يمكنك تخصيصه.

إنشاء البرنامج المساعد

المكوّن الإضافي لمكتبة Car UI هو ملف 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",
}

جرادل

شاهد ملف 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" يجعل المكون الإضافي قابلاً للاكتشاف في مكتبة Car UI. يجب تصدير الموفر حتى يمكن الاستعلام عنه في وقت التشغيل. وأيضًا، إذا تم تعيين السمة 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 تتم إضافة فئاتها تلقائيًا إلى أداة تحميل الفصل المشتركة بين التطبيقات. عندما يحدد تطبيق يستخدم مكتبة Car UI تبعية وقت التشغيل للمكتبة المشتركة للمكون الإضافي، يمكن لمحمل الفصل الخاص به الوصول إلى فئات المكتبة المشتركة للمكون الإضافي. يمكن للمكونات الإضافية التي يتم تنفيذها كتطبيقات Android عادية (وليست مكتبة مشتركة) أن تؤثر سلبًا على أوقات بدء تشغيل التطبيق البارد.

تنفيذ وبناء المكتبات المشتركة

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

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

الاعتماد على المكتبات المشتركة

بالنسبة لكل تطبيق على النظام يستخدم مكتبة Car UI، قم بتضمين علامة 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 عن خطأ مفاده أنه لا يمكنه العثور على نشاط رئيسي لتشغيله. هذا أمر متوقع، لأن المكون الإضافي لا يحتوي على أي أنشطة (باستثناء القصد الفارغ المستخدم لحل القصد). للتخلص من الخطأ، قم بتغيير خيار التشغيل إلى لا شيء في تكوين البنية.

تكوين البرنامج المساعد Android Studio الشكل 1. تكوين البرنامج المساعد Android Studio

البرنامج المساعد الوكيل

يتطلب تخصيص التطبيقات باستخدام مكتبة Car UI وجود RRO يستهدف كل تطبيق محدد سيتم تعديله، بما في ذلك عندما تكون التخصيصات متطابقة عبر التطبيقات. وهذا يعني أن RRO مطلوب لكل تطبيق. تعرف على التطبيقات التي تستخدم مكتبة Car UI.

يعد البرنامج المساعد الوكيل لمكتبة Car UI مثالاً للمكتبة المشتركة للمكونات الإضافية التي تقوم بتفويض تطبيقات المكونات الخاصة بها إلى الإصدار الثابت من مكتبة Car UI. يمكن استهداف هذا المكون الإضافي باستخدام RRO، والذي يمكن استخدامه كنقطة تخصيص واحدة للتطبيقات التي تستخدم مكتبة Car UI دون الحاجة إلى تنفيذ مكون إضافي وظيفي. لمزيد من المعلومات حول RROs، راجع تغيير قيمة موارد التطبيق في وقت التشغيل .

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

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

تنفيذ واجهات برمجة تطبيقات البرنامج المساعد

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

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

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

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

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

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

للحفاظ على التوافق مع التطبيقات التي تم تجميعها مقابل الإصدارات الأقدم من مكتبة Car Ui الثابتة، يوصى بدعم maxVersion s من 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 قيمة فارغة. بالنسبة لمعظم المكونات الإضافية، هذا هو كل ما يجب حدوثه، ولكن إذا كان مؤلف البرنامج الإضافي يرغب في تطبيق زخرفة حول حافة التطبيق، على سبيل المثال، فلا يزال من الممكن القيام بذلك باستخدام تخطيط أساسي. تعتبر هذه الزخارف مفيدة بشكل خاص للأجهزة ذات الشاشات غير المستطيلة، حيث يمكنها دفع التطبيق إلى مساحة مستطيلة وإضافة انتقالات نظيفة إلى المساحة غير المستطيلة.

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

تمرير المحتوى أسفل شريط الأدوات الشكل 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 القيمة null عندما تكون toolbarEnabled false ، ولكي يشير المكون الإضافي إلى أنه لا يرغب في تخصيص التخطيط الأساسي، فيجب أن يُرجع false من customizesBaseLayout .

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

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

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

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

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

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

AppStyledView

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

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

السياقات

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

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

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

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

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

جيتباك يؤلف

يمكن تنفيذ المكونات الإضافية باستخدام 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)
//  }
}