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

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

कोई प्लगिन बनाना

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

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

सिस्टम पर मौजूद हर उस ऐप्लिकेशन के लिए, ऐप्लिकेशन मेनिफ़ेस्ट में uses-library टैग शामिल करें जो Car UI लाइब्रेरी का इस्तेमाल करता है. इसे 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 ऐप्लिकेशन को इंस्टॉल करने की प्रोसेस में एक बग मौजूद है. इसकी वजह से, किसी प्लगिन को अपडेट करने पर, वह अपडेट लागू नहीं होता. इस समस्या को ठीक करने के लिए, प्लगिन के बिल्ड कॉन्फ़िगरेशन में जाकर, Always install with package manager (disables deploy optimizations on Android 11 and later) विकल्प चुनें.

इसके अलावा, प्लगिन इंस्टॉल करते समय Android Studio एक गड़बड़ी की सूचना देता है. इसमें बताया जाता है कि वह लॉन्च करने के लिए मुख्य गतिविधि नहीं ढूंढ सका. ऐसा इसलिए है, क्योंकि प्लगिन में कोई गतिविधि नहीं है. इसमें सिर्फ़ खाली इंटेंट है, जिसका इस्तेमाल इंटेंट को हल करने के लिए किया जाता है. गड़बड़ी को ठीक करने के लिए, बिल्ड कॉन्फ़िगरेशन में लॉन्च करें विकल्प को बदलकर कुछ नहीं करें.

Android Studio में प्लगिन कॉन्फ़िगर करना पहली इमेज. Android Studio में प्लगिन कॉन्फ़िगर करना

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

टूलबार

बेस लेआउट

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

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

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

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

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

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

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

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

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

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

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

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

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

कॉन्टेक्स्ट

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

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

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

मोड में बदलाव

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

Jetpack Compose

प्लगिन को Jetpack Compose का इस्तेमाल करके लागू किया जा सकता है. हालांकि, यह ऐल्फ़ा लेवल की सुविधा है और इसे स्टेबल नहीं माना जाना चाहिए.

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

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

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)
//  }
}