कार के यूज़र इंटरफ़ेस (यूआई) के प्लगिन

कार की यूज़र इंटरफ़ेस (यूआई) लाइब्रेरी में, कॉम्पोनेंट को पसंद के मुताबिक बनाने की सुविधा को पूरी तरह से लागू करने के लिए, कार की यूज़र इंटरफ़ेस (यूआई) लाइब्रेरी के प्लग इन का इस्तेमाल करें. इसके लिए, रनटाइम रिसॉर्स ओवरले (आरआरओ) का इस्तेमाल न करें. आरआरओ की मदद से, कार की यूज़र इंटरफ़ेस (यूआई) लाइब्रेरी के कॉम्पोनेंट के सिर्फ़ एक्सएमएल संसाधनों में बदलाव किया जा सकता है. इससे, कार की यूज़र इंटरफ़ेस (यूआई) लाइब्रेरी को पसंद के मुताबिक बनाने की सुविधा सीमित हो जाती है.

प्लग इन बनाना

कार की यूज़र इंटरफ़ेस (यूआई) लाइब्रेरी का प्लग इन एक ऐसा 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 ऐप्लिकेशन बनाने जैसा ही है. हालांकि, इन दोनों में कुछ अंतर भी हैं.

  • अपने प्लग इन के ऐप्लिकेशन मेनिफ़ेस्ट में, application टैग के नीचे library टैग का इस्तेमाल करें. साथ ही, प्लग इन के पैकेज का नाम डालें:
    <application>
        <library android:name="com.chassis.car.ui.plugin" />
        ...
    </application>
  • AAPT फ़्लैग shared-lib की मदद से, Soong android_app के बिल्कुल नए नियम (Android.bp) को कॉन्फ़िगर करें. इसका इस्तेमाल, शेयर की गई लाइब्रेरी बनाने के लिए किया जाता है:
android_app {
  ...
  aaptflags: ["--shared-lib"],
  ...
}

शेयर की गई लाइब्रेरी पर निर्भरता

सिस्टम पर मौजूद हर उस ऐप्लिकेशन के लिए, ऐप्लिकेशन मेनिफ़ेस्ट में application टैग के नीचे, प्लग इन पैकेज के नाम के साथ uses-library टैग शामिल करें जो कार की यूज़र इंटरफ़ेस (यूआई) लाइब्रेरी का इस्तेमाल करता है:

<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 कॉन्फ़िगरेशन पहली इमेज. प्लग इन का Android Studio कॉन्फ़िगरेशन

प्रॉक्सी प्लग इन

कार की यूज़र इंटरफ़ेस (यूआई) लाइब्रेरी का इस्तेमाल करने वाले ऐप्लिकेशन को पसंद के मुताबिक बनाने के लिए, आरआरओ की ज़रूरत होती है. यह आरआरओ, उन सभी ऐप्लिकेशन को टारगेट करता है जिनमें बदलाव करना है. इसमें, उन ऐप्लिकेशन को भी टारगेट किया जाता है जिनमें सभी ऐप्लिकेशन के लिए एक जैसे बदलाव किए जाते हैं. इसका मतलब है कि हर ऐप्लिकेशन के लिए आरआरओ ज़रूरी है. देखें कि कौनसे ऐप्लिकेशन, कार की यूज़र इंटरफ़ेस (यूआई) लाइब्रेरी का इस्तेमाल करते हैं.

कार की यूज़र इंटरफ़ेस (यूआई) लाइब्रेरी का प्रॉक्सी प्लग इन, प्लग इन की शेयर की गई लाइब्रेरी का एक उदाहरण है. यह अपने कॉम्पोनेंट को लागू करने की ज़िम्मेदारी, कार की यूज़र इंटरफ़ेस (यूआई) लाइब्रेरी के स्टैटिक वर्शन को सौंपता है. इस प्लग इन को आरआरओ के साथ टारगेट किया जा सकता है. इसका इस्तेमाल, कार की यूज़र इंटरफ़ेस (यूआई) लाइब्रेरी का इस्तेमाल करने वाले ऐप्लिकेशन के लिए, एक ही जगह से कस्टमाइज़ करने के तौर पर किया जा सकता है. इसके लिए, किसी फ़ंक्शनल प्लग इन को लागू करने की ज़रूरत नहीं होती. आरआरओ के बारे में ज़्यादा जानकारी के लिए, रनटाइम के दौरान ऐप्लिकेशन के संसाधनों की वैल्यू बदलना लेख पढ़ें.

प्लग इन का इस्तेमाल करके, पसंद के मुताबिक बनाने के लिए, प्रॉक्सी प्लग इन सिर्फ़ एक उदाहरण और शुरुआती पॉइंट है. आरआरओ के अलावा, अपनी पसंद के मुताबिक बनाने के लिए, प्लग इन के कॉम्पोनेंट का सबसेट लागू किया जा सकता है. साथ ही, बाकी कॉम्पोनेंट के लिए प्रॉक्सी प्लग इन का इस्तेमाल किया जा सकता है. इसके अलावा, सभी प्लग इन कॉम्पोनेंट को शुरू से लागू किया जा सकता है.

प्रॉक्सी प्लग इन, ऐप्लिकेशन के लिए आरआरओ को पसंद के मुताबिक बनाने की सुविधा देता है. हालांकि, प्लग इन का इस्तेमाल करने से ऑप्ट-आउट करने वाले ऐप्लिकेशन के लिए, अब भी आरआरओ की ज़रूरत होगी. यह आरआरओ, सीधे तौर पर ऐप्लिकेशन को टारगेट करेगा.

प्लग इन एपीआई लागू करना

प्लग इन का मुख्य एंट्री पॉइंट, com.android.car.ui.plugin.PluginVersionProviderImpl क्लास है. सभी प्लग इन में, इस नाम और पैकेज के नाम वाली क्लास शामिल होनी चाहिए. इस क्लास में डिफ़ॉल्ट कंस्ट्रक्टर होना चाहिए और PluginVersionProviderOEMV1 इंटरफ़ेस लागू करना चाहिए.

CarUi प्लग इन, प्लग इन से पुराने या नए ऐप्लिकेशन के साथ काम करने चाहिए. इसे आसान बनाने के लिए, सभी प्लग इन एपीआई के क्लास के आखिर में V# जोड़ा जाता है. अगर कार की यूज़र इंटरफ़ेस (यूआई) लाइब्रेरी का नया वर्शन, नई सुविधाओं के साथ रिलीज़ किया जाता है, तो वे कॉम्पोनेंट के V2 वर्शन का हिस्सा होते हैं. कार की यूज़र इंटरफ़ेस (यूआई) लाइब्रेरी, नई सुविधाओं को पुराने प्लग इन कॉम्पोनेंट के दायरे में काम करने के लिए, पूरी कोशिश करती है. उदाहरण के लिए, टूलबार में किसी नए तरह के बटन को MenuItems में बदलकर.

हालांकि, कार की यूज़र इंटरफ़ेस (यूआई) लाइब्रेरी के पुराने वर्शन वाला ऐप्लिकेशन, नए एपीआई के लिए लिखे गए नए प्लग इन के साथ काम नहीं कर सकता. इस समस्या को हल करने के लिए, हम प्लग इन को अपने अलग-अलग वर्शन दिखाने की अनुमति देते हैं. यह अनुमति, ऐप्लिकेशन के साथ काम करने वाले OEM एपीआई के वर्शन के आधार पर दी जाती है.

PluginVersionProviderOEMV1 में एक तरीका है:

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

यह मेथड एक ऑब्जेक्ट दिखाता है, जो प्लग इन के साथ काम करने वाले PluginFactoryOEMV# के सबसे नए वर्शन को लागू करता है. हालांकि, यह वर्शन maxVersion से कम या उसके बराबर ही होता है. अगर किसी प्लग इन में, PluginFactory को लागू करने का तरीका इतना पुराना नहीं है, तो वह null दिखा सकता है. ऐसे में, CarUi कॉम्पोनेंट के स्टैटिक तौर पर लिंक किए गए तरीके का इस्तेमाल किया जाता है.

स्टैटिक कार यूज़र इंटरफ़ेस लाइब्रेरी के पुराने वर्शन के लिए कॉम्पाइल किए गए ऐप्लिकेशन के साथ काम करने की सुविधा बनाए रखने के लिए, हमारा सुझाव है कि आप अपने प्लग इन में PluginVersionProvider क्लास के लागू होने से, maxVersion के 2, 5, और उसके बाद के वर्शन का इस्तेमाल करें. वर्शन 1, 3, और 4 काम नहीं करते. ज़्यादा जानकारी के लिए, PluginVersionProviderImpl देखें.

PluginFactory एक इंटरफ़ेस है, जो CarUi के सभी अन्य कॉम्पोनेंट बनाता है. इससे यह भी तय होता है कि उनके इंटरफ़ेस के किस वर्शन का इस्तेमाल किया जाना चाहिए. अगर प्लग इन इनमें से किसी भी कॉम्पोनेंट को लागू नहीं करता है, तो वह अपने बनाने वाले फ़ंक्शन में null दिखा सकता है. हालांकि, टूलबार के लिए ऐसा नहीं होता, क्योंकि उसके पास एक अलग customizesBaseLayout() फ़ंक्शन होता है.

pluginFactory से यह तय होता है कि CarUi कॉम्पोनेंट के कौनसे वर्शन एक साथ इस्तेमाल किए जा सकते हैं. उदाहरण के लिए, ऐसा कभी नहीं होगा कि कोई pluginFactory, Toolbar का वर्शन 100 और RecyclerView का वर्शन 1, दोनों बना सके. इसकी वजह यह है कि इस बात की कोई गारंटी नहीं है कि कॉम्पोनेंट के अलग-अलग वर्शन एक साथ काम करेंगे. टूलबार के वर्शन 100 का इस्तेमाल करने के लिए, डेवलपर को pluginFactory के ऐसे वर्शन को लागू करना होगा जिससे टूलबार का वर्शन 100 बनता हो. इससे, बनाए जा सकने वाले अन्य कॉम्पोनेंट के वर्शन के विकल्प सीमित हो जाते हैं. हो सकता है कि दूसरे कॉम्पोनेंट के वर्शन एक जैसे न हों. उदाहरण के लिए, pluginFactoryOEMV100 से ToolbarControllerOEMV100 और RecyclerViewOEMV70 बन सकते हैं.

टूलबार

बेस लेआउट

टूलबार और "बेस लेआउट" काफ़ी हद तक एक-दूसरे से जुड़े होते हैं. इसलिए, टूलबार बनाने वाले फ़ंक्शन को installBaseLayoutAround कहा जाता है. बेस लेआउट एक ऐसा कॉन्सेप्ट है जिसकी मदद से, टूलबार को ऐप्लिकेशन के कॉन्टेंट के आस-पास कहीं भी रखा जा सकता है. इससे, ऐप्लिकेशन के सबसे ऊपर/नीचे, वर्टिकल तौर पर किनारों पर या पूरे ऐप्लिकेशन को घेरने वाले सर्कुलर टूलबार को रखा जा सकता है. ऐसा करने के लिए, installBaseLayoutAround को एक व्यू पास करके, टूलबार/बेस लेआउट को चारों ओर लपेटा जाता है.

प्लग इन को दिए गए व्यू को अपने पैरंट से अलग करना चाहिए. साथ ही, पैरंट के उसी इंडेक्स में प्लग इन के लेआउट को फ़ुल फ़ॉर्मैट में बदलना चाहिए और उसी LayoutParams के साथ बदलना चाहिए जिससे व्यू को अभी-अभी अलग किया गया था. इसके बाद, व्यू को अभी-अभी फ़ुल फ़ॉर्मैट में बदले गए लेआउट में कहीं भी फिर से अटैच करना चाहिए. अगर ऐप्लिकेशन ने अनुरोध किया है, तो बड़े किए गए लेआउट में टूलबार शामिल होगा.

ऐप्लिकेशन, टूलबार के बिना बुनियादी लेआउट का अनुरोध कर सकता है. अगर ऐसा होता है, तो installBaseLayoutAround को शून्य दिखाना चाहिए. ज़्यादातर प्लग इन के लिए, बस यही करना होता है. हालांकि, अगर प्लग इन के लेखक को ऐप्लिकेशन के किनारे पर कोई सजावट लागू करनी है, तो वह बेस लेआउट का इस्तेमाल करके ऐसा कर सकता है. ये डिज़ाइन, खास तौर पर ऐसे डिवाइसों के लिए काम के होते हैं जिनकी स्क्रीन, आयताकार नहीं होती. इनकी मदद से, ऐप्लिकेशन को आयताकार स्पेस में दिखाया जा सकता है. साथ ही, आयताकार नहीं होने वाले स्पेस में भी ऐप्लिकेशन को बेहतर तरीके से ट्रांज़िशन किया जा सकता है.

installBaseLayoutAround को भी Consumer<InsetsOEMV1> पास किया गया है. इस consumer का इस्तेमाल, ऐप्लिकेशन को यह बताने के लिए किया जा सकता है कि प्लग इन, ऐप्लिकेशन के कॉन्टेंट को टूलबार या किसी अन्य तरीके से आंशिक रूप से कवर कर रहा है. इसके बाद, ऐप्लिकेशन को पता चल जाएगा कि उसे इस स्पेस में ड्रॉ करना जारी रखना है. हालांकि, इसमें ऐसे ज़रूरी कॉम्पोनेंट शामिल नहीं करने हैं जिनसे उपयोगकर्ता इंटरैक्ट कर सकते हैं. इस इफ़ेक्ट का इस्तेमाल हमारे रेफ़रंस डिज़ाइन में किया गया है, ताकि टूलबार को आधा पारदर्शी बनाया जा सके और उसके नीचे सूचियां स्क्रोल की जा सकें. अगर इस सुविधा को लागू नहीं किया गया था, तो सूची में मौजूद पहला आइटम टूलबार के नीचे दिखता था और उस पर क्लिक नहीं किया जा सकता था. अगर इस असर की ज़रूरत नहीं है, तो प्लग इन, Consumer को अनदेखा कर सकता है.

टूलबार के नीचे स्क्रोल किया जा रहा कॉन्टेंट दूसरी इमेज. टूलबार के नीचे स्क्रोल किया जा रहा कॉन्टेंट

ऐप्लिकेशन के हिसाब से, जब प्लग इन नए इनसेट भेजता है, तो उन्हें InsetsChangedListener लागू करने वाली किसी भी गतिविधि या फ़्रैगमेंट से मिलेगा. अगर कोई गतिविधि या फ़्रैगमेंट InsetsChangedListener लागू नहीं करता है, तो कार यूज़र इंटरफ़ेस (यूआई) लाइब्रेरी डिफ़ॉल्ट रूप से इनसेट को मैनेज करेगी. इसके लिए, वह इनसेट को फ़्रैगमेंट वाले 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 हिंट दिया जाता है. इसका इस्तेमाल यह बताने के लिए किया जाता है कि जिस व्यू को रैप किया जाना चाहिए वह पूरे ऐप्लिकेशन को कवर करता है या सिर्फ़ एक छोटे से सेक्शन को. इसका इस्तेमाल, किनारे पर कुछ ऐसी सजावट लागू करने से बचने के लिए किया जा सकता है जो सिर्फ़ पूरी स्क्रीन के किनारे पर दिखने पर ही सही लगती हैं. सेटिंग एक ऐसा ऐप्लिकेशन है जो नॉन-फ़ुलस्क्रीन बेस लेआउट का इस्तेमाल करता है. इसमें ड्यूअल-पैन लेआउट के हर पैनल का अपना टूलबार होता है.

toolbarEnabled के false होने पर, installBaseLayoutAround के लिए यह ज़रूरी है कि वह कोई वैल्यू न दिखाए. अगर प्लग इन को यह बताना है कि उसे बुनियादी लेआउट में बदलाव नहीं करना है, तो उसे customizesBaseLayout से false दिखाना होगा.

रोटरी कंट्रोल की पूरी तरह से मदद करने के लिए, बुनियादी लेआउट में एक FocusParkingView और एक FocusArea होना चाहिए. जिन डिवाइसों पर रोटरी विकल्प काम नहीं करता उन पर ये व्यू नहीं दिखाए जा सकते. FocusParkingView/FocusAreas को स्टैटिक CarUi लाइब्रेरी में लागू किया जाता है. इसलिए, कॉन्टेक्स्ट से व्यू बनाने के लिए फ़ैक्ट्री उपलब्ध कराने के लिए, setRotaryFactories का इस्तेमाल किया जाता है.

फ़ोकस व्यू बनाने के लिए इस्तेमाल किए गए कॉन्टेक्स्ट, प्लग इन के कॉन्टेक्स्ट के बजाय सोर्स कॉन्टेक्स्ट होने चाहिए. FocusParkingView को ट्री में पहले व्यू के सबसे करीब होना चाहिए, क्योंकि जब उपयोगकर्ता को कोई फ़ोकस नहीं दिखना चाहिए, तब उस पर फ़ोकस किया जाता है. FocusArea को टूलबार को बेस लेआउट में रैप करना चाहिए, ताकि यह पता चल सके कि यह रोटरी नज़्ड ज़ोन है. अगर FocusArea का इस्तेमाल नहीं किया जाता है, तो उपयोगकर्ता रोटरी कंट्रोलर की मदद से, टूलबार में मौजूद किसी भी बटन पर नहीं जा सकता.

टूलबार कंट्रोलर

दिखाए गए ToolbarController को लागू करना, बुनियादी लेआउट के मुकाबले ज़्यादा आसान होना चाहिए. इसका काम, अपने सेटर को दी गई जानकारी को लेना और उसे बेस लेआउट में दिखाना है. ज़्यादातर तरीकों के बारे में जानकारी के लिए, Javadoc देखें. यहां कुछ ज़्यादा मुश्किल तरीकों के बारे में बताया गया है.

getImeSearchInterface का इस्तेमाल, IME (कीबोर्ड) विंडो में खोज के नतीजे दिखाने के लिए किया जाता है. यह कीबोर्ड के साथ-साथ खोज के नतीजों को दिखाने/ऐनिमेट करने के लिए मददगार हो सकता है. उदाहरण के लिए, अगर कीबोर्ड ने सिर्फ़ आधी स्क्रीन पर जगह ली है. ज़्यादातर फ़ंक्शन, स्टैटिक CarUi लाइब्रेरी में लागू किए जाते हैं. प्लग इन में मौजूद खोज इंटरफ़ेस, स्टैटिक लाइब्रेरी के लिए सिर्फ़ TextView और onPrivateIMECommand कॉलबैक पाने के तरीके उपलब्ध कराता है. इसके लिए, प्लग इन को TextView सबक्लास का इस्तेमाल करना चाहिए, जो onPrivateIMECommand को बदल देता है और कॉल को खोज बार के TextView के तौर पर दिए गए लिसनर को पास करता है.

setMenuItems, स्क्रीन पर सिर्फ़ मेन्यू आइटम दिखाता है. हालांकि, इसे अक्सर कॉल किया जाएगा. मेन्यू आइटम के लिए प्लग इन एपीआई में बदलाव नहीं किया जा सकता. इसलिए, जब भी कोई मेन्यू आइटम बदला जाता है, तो एक नया setMenuItems कॉल होगा. ऐसा किसी मामूली वजह से भी हो सकता है. जैसे, जब उपयोगकर्ता ने स्विच मेन्यू आइटम पर क्लिक किया हो और उस क्लिक की वजह से स्विच टॉगल हो गया हो. इसलिए, परफ़ॉर्मेंस और ऐनिमेशन, दोनों के लिहाज़ से, हमारा सुझाव है कि आप मेन्यू आइटम की पुरानी और नई सूची के बीच के अंतर का हिसाब लगाएं. साथ ही, सिर्फ़ उन व्यू को अपडेट करें जिनमें असल में बदलाव हुआ है. MenuItems में एक key फ़ील्ड होता है, जिससे इस काम में मदद मिल सकती है. इसकी वजह यह है कि एक ही MenuItem के लिए, 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 का इस्तेमाल करके लागू किया जा सकता है. हालांकि, यह सुविधा अल्फा-लेवल पर है और इसे स्टेबल नहीं माना जाना चाहिए.

प्लग इन, रेंडर करने के लिए Compose की सुविधा वाला प्लैटफ़ॉर्म बनाने के लिए, ComposeView का इस्तेमाल कर सकते हैं. यह ComposeView, कॉम्पोनेंट में getView तरीके से ऐप्लिकेशन को वापस भेजा गया डेटा होगा.

ComposeView का इस्तेमाल करने में एक बड़ी समस्या यह है कि यह लेआउट में रूट व्यू पर टैग सेट करता है, ताकि ग्लोबल वैरिएबल को स्टोर किया जा सके. ये वैरिएबल, हैरारकी में अलग-अलग ComposeView के साथ शेयर किए जाते हैं. प्लग इन के रिसॉर्स आईडी, ऐप्लिकेशन के रिसॉर्स आईडी से अलग नेमस्पेस में नहीं होते. इसलिए, जब ऐप्लिकेशन और प्लग इन, दोनों एक ही व्यू पर टैग सेट करते हैं, तो इससे समस्याएं हो सकती हैं. यहां एक कस्टम 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)
//  }
}