গাড়ী UI প্লাগইন

রানটাইম রিসোর্স ওভারলে (RRO) ব্যবহার করার পরিবর্তে, কার UI লাইব্রেরিতে কম্পোনেন্ট কাস্টমাইজেশনের সম্পূর্ণ বাস্তবায়ন তৈরি করতে কার UI লাইব্রেরি প্লাগইন ব্যবহার করুন। RRO আপনাকে শুধুমাত্র কার UI লাইব্রেরি কম্পোনেন্টগুলোর XML রিসোর্স পরিবর্তন করার সুযোগ দেয়, যা আপনার কাস্টমাইজেশনের পরিধিকে সীমিত করে।

একটি প্লাগইন তৈরি করুন

একটি কার UI লাইব্রেরি প্লাগইন হলো এমন একটি APK যাতে কিছু প্লাগইন API বাস্তবায়নকারী ক্লাস থাকে। এই প্লাগইন API-গুলোকে একটি স্ট্যাটিক লাইব্রেরি হিসেবে প্লাগইনে কম্পাইল করা যায়।

Soong এবং Gradle-এ উদাহরণ দেখুন:

সুং

এই সুং উদাহরণটি বিবেচনা করুন:

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" প্লাগইনটিকে কার 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>

অবশেষে, নিরাপত্তা ব্যবস্থা হিসেবে আপনার অ্যাপটি সাইন করুন

একটি শেয়ার্ড লাইব্রেরি হিসাবে প্লাগইন

অ্যান্ড্রয়েড স্ট্যাটিক লাইব্রেরিগুলো সরাসরি অ্যাপের মধ্যে কম্পাইল করা হলেও, অ্যান্ড্রয়েড শেয়ার্ড লাইব্রেরিগুলো একটি স্বতন্ত্র APK-তে কম্পাইল করা হয়, যা রানটাইমে অন্যান্য অ্যাপ দ্বারা ব্যবহৃত হয়।

যেসব প্লাগইন অ্যান্ড্রয়েড শেয়ার্ড লাইব্রেরি হিসেবে প্রয়োগ করা হয়, সেগুলোর ক্লাসগুলো অ্যাপগুলোর মধ্যে শেয়ার্ড ক্লাসলোডারে স্বয়ংক্রিয়ভাবে যুক্ত হয়ে যায়। যখন কার UI লাইব্রেরি ব্যবহারকারী কোনো অ্যাপ প্লাগইন শেয়ার্ড লাইব্রেরির উপর একটি রানটাইম নির্ভরতা নির্দিষ্ট করে, তখন তার ক্লাসলোডার প্লাগইন শেয়ার্ড লাইব্রেরির ক্লাসগুলো অ্যাক্সেস করতে পারে। সাধারণ অ্যান্ড্রয়েড অ্যাপ হিসেবে প্রয়োগ করা প্লাগইন (শেয়ার্ড লাইব্রেরি নয়) অ্যাপের কোল্ড স্টার্ট টাইমের উপর নেতিবাচক প্রভাব ফেলতে পারে।

শেয়ার্ড লাইব্রেরি বাস্তবায়ন এবং নির্মাণ করুন

অ্যান্ড্রয়েড শেয়ার্ড লাইব্রেরি ব্যবহার করে ডেভেলপ করা অনেকটাই সাধারণ অ্যান্ড্রয়েড অ্যাপ তৈরির মতোই, তবে কয়েকটি গুরুত্বপূর্ণ পার্থক্য রয়েছে।

  • আপনার প্লাগইনের অ্যাপ ম্যানিফেস্টে, application ট্যাগের অধীনে library ট্যাগটি ব্যবহার করুন এবং এর সাথে প্লাগইন প্যাকেজের নামটি যুক্ত করুন:
    <application>
        <library android:name="com.chassis.car.ui.plugin" />
        ...
    </application>
  • আপনার Soong android_app বিল্ড রুল ( Android.bp ) AAPT ফ্ল্যাগ shared-lib দিয়ে কনফিগার করুন, যা একটি শেয়ার্ড লাইব্রেরি বিল্ড করতে ব্যবহৃত হয়:
android_app {
  ...
  aaptflags: ["--shared-lib"],
  ...
}

শেয়ার করা লাইব্রেরির উপর নির্ভরতা

সিস্টেমের প্রতিটি অ্যাপের জন্য যেগুলো কার UI লাইব্রেরি ব্যবহার করে, অ্যাপ ম্যানিফেস্টে application ট্যাগের অধীনে প্লাগইন প্যাকেজের নামসহ uses-library ট্যাগটি অন্তর্ভুক্ত করুন:

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

একটি প্লাগইন ইনস্টল করুন

PRODUCT_PACKAGES এ মডিউলটি অন্তর্ভুক্ত করে প্লাগইনগুলি অবশ্যই সিস্টেম পার্টিশনে আগে থেকে ইনস্টল করতে হবে। আগে থেকে ইনস্টল করা প্যাকেজটি অন্য যেকোনো ইনস্টল করা অ্যাপের মতোই আপডেট করা যেতে পারে।

আপনি যদি সিস্টেমে থাকা কোনো প্লাগইন আপডেট করেন, তবে সেই প্লাগইনটি ব্যবহারকারী অ্যাপগুলো স্বয়ংক্রিয়ভাবে বন্ধ হয়ে যায়। ব্যবহারকারী পুনরায় খুললে, আপডেট করা পরিবর্তনগুলো দেখতে পান। অ্যাপটি যদি চালু না থাকে, তবে পরেরবার চালু হলে এতে আপডেট করা প্লাগইনটি যুক্ত হয়ে যায়।

অ্যান্ড্রয়েড স্টুডিও দিয়ে প্লাগইন ইনস্টল করার সময় কিছু অতিরিক্ত বিষয় বিবেচনায় রাখতে হবে। এই লেখাটি লেখার সময়, অ্যান্ড্রয়েড স্টুডিও অ্যাপ ইনস্টলেশন প্রক্রিয়ায় একটি বাগ রয়েছে, যার কারণে প্লাগইনের আপডেটগুলো কার্যকর হয় না। প্লাগইনের বিল্ড কনফিগারেশনে ‘Always install with package manager’ অপশনটি নির্বাচন করার মাধ্যমে এটি সমাধান করা যেতে পারে (এটি অ্যান্ড্রয়েড ১১ এবং তার পরবর্তী সংস্করণগুলোতে ডিপ্লয় অপটিমাইজেশন নিষ্ক্রিয় করে)

এছাড়াও, প্লাগইনটি ইনস্টল করার সময়, অ্যান্ড্রয়েড স্টুডিও একটি ত্রুটি দেখায় যে এটি চালু করার জন্য কোনো প্রধান অ্যাক্টিভিটি খুঁজে পাচ্ছে না। এটি প্রত্যাশিত, কারণ প্লাগইনটির কোনো অ্যাক্টিভিটি নেই (একটি ইন্টেন্ট রিজলভ করার জন্য ব্যবহৃত খালি ইন্টেন্টটি ছাড়া)। ত্রুটিটি দূর করতে, বিল্ড কনফিগারেশনে লঞ্চ অপশনটি 'Nothing'- এ পরিবর্তন করুন।

প্লাগইন অ্যান্ড্রয়েড স্টুডিও কনফিগারেশন চিত্র ১. প্লাগইন অ্যান্ড্রয়েড স্টুডিও কনফিগারেশন

প্রক্সি প্লাগইন

কার UI লাইব্রেরি ব্যবহার করে অ্যাপ কাস্টমাইজ করার জন্য একটি RRO প্রয়োজন, যা পরিবর্তন করা হবে এমন প্রতিটি নির্দিষ্ট অ্যাপকে লক্ষ্য করে তৈরি করতে হবে; এমনকি যখন বিভিন্ন অ্যাপের মধ্যে কাস্টমাইজেশন একই রকম হয়, তখনও এটি প্রযোজ্য। এর মানে হলো, প্রতিটি অ্যাপের জন্য একটি করে RRO প্রয়োজন। দেখুন কোন কোন অ্যাপ কার UI লাইব্রেরি ব্যবহার করে।

কার UI লাইব্রেরি প্রক্সি প্লাগইন হলো একটি উদাহরণ প্লাগইন শেয়ার্ড লাইব্রেরি যা এর কম্পোনেন্ট ইমপ্লিমেন্টেশনগুলোকে কার UI লাইব্রেরির স্ট্যাটিক সংস্করণের কাছে অর্পণ করে। এই প্লাগইনটিকে একটি RRO (রিমোট রিসোর্স অর্গানাইজেশন) দ্বারা টার্গেট করা যেতে পারে, যা কার UI লাইব্রেরি ব্যবহারকারী অ্যাপগুলোর জন্য একটি একক কাস্টমাইজেশন পয়েন্ট হিসেবে কাজ করে এবং এর জন্য কোনো ফাংশনাল প্লাগইন ইমপ্লিমেন্ট করার প্রয়োজন হয় না। RRO সম্পর্কে আরও তথ্যের জন্য, "রানটাইমে একটি অ্যাপের রিসোর্সের মান পরিবর্তন করুন" দেখুন।

প্রক্সি প্লাগইনটি হলো প্লাগইন ব্যবহার করে কাস্টমাইজেশন করার জন্য শুধুমাত্র একটি উদাহরণ এবং সূচনা বিন্দু। RRO-এর বাইরে কাস্টমাইজেশনের জন্য, কেউ প্লাগইন কম্পোনেন্টগুলোর একটি উপসেট ইমপ্লিমেন্ট করে বাকিগুলোর জন্য প্রক্সি প্লাগইন ব্যবহার করতে পারেন, অথবা প্লাগইনের সমস্ত কম্পোনেন্ট একেবারে গোড়া থেকে ইমপ্লিমেন্ট করতে পারেন।

যদিও প্রক্সি প্লাগইনটি অ্যাপগুলির জন্য RRO কাস্টমাইজেশনের একটি একক সুযোগ প্রদান করে, যেসব অ্যাপ প্লাগইনটি ব্যবহার না করার সিদ্ধান্ত নেয়, তাদের জন্যও এমন একটি RRO-এর প্রয়োজন হবে যা সরাসরি অ্যাপটিকেই লক্ষ্য করে।

প্লাগইন এপিআইগুলো বাস্তবায়ন করুন

প্লাগইনটির প্রধান এন্ট্রি পয়েন্ট হলো com.android.car.ui.plugin.PluginVersionProviderImpl ক্লাসটি। সকল প্লাগইনে অবশ্যই এই নির্দিষ্ট নাম এবং প্যাকেজ নামের একটি ক্লাস অন্তর্ভুক্ত থাকতে হবে। এই ক্লাসটিতে একটি ডিফল্ট কনস্ট্রাক্টর থাকতে হবে এবং এটিকে PluginVersionProviderOEMV1 ইন্টারফেসটি ইমপ্লিমেন্ট করতে হবে।

CarUi প্লাগইনগুলোকে অবশ্যই প্লাগইনটির চেয়ে পুরোনো বা নতুন অ্যাপের সাথে কাজ করতে হবে। এই কাজটি সহজ করার জন্য, সমস্ত প্লাগইন API-এর ক্লাসনেমের শেষে V# যোগ করে ভার্সন নির্ধারণ করা হয়। যদি Car UI লাইব্রেরির কোনো নতুন সংস্করণ নতুন ফিচারসহ প্রকাশিত হয়, তবে সেগুলো কম্পোনেন্টটির V2 সংস্করণের অংশ হয়ে যায়। Car UI লাইব্রেরি পুরোনো প্লাগইন কম্পোনেন্টের আওতার মধ্যে নতুন ফিচারগুলোকে কার্যকর করার জন্য যথাসাধ্য চেষ্টা করে। উদাহরণস্বরূপ, টুলবারের একটি নতুন ধরনের বাটনকে MenuItems এ রূপান্তর করার মাধ্যমে।

তবে, কার UI লাইব্রেরির পুরোনো সংস্করণ ব্যবহারকারী কোনো অ্যাপ নতুন API-এর ওপর ভিত্তি করে লেখা নতুন প্লাগইনের সাথে খাপ খাইয়ে নিতে পারে না। এই সমস্যা সমাধানের জন্য, আমরা প্লাগইনগুলোকে অ্যাপ দ্বারা সমর্থিত OEM API-এর সংস্করণের ওপর ভিত্তি করে নিজেদের ভিন্ন ভিন্ন ইমপ্লিমেন্টেশন ফেরত দেওয়ার সুযোগ দিই।

PluginVersionProviderOEMV1 এর মধ্যে একটি মেথড আছে:

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

এই মেথডটি এমন একটি অবজেক্ট রিটার্ন করে যা প্লাগইন দ্বারা সমর্থিত PluginFactoryOEMV# এর সর্বোচ্চ সংস্করণটি ইমপ্লিমেন্ট করে, এবং যা maxVersion এর চেয়ে কম বা সমান। যদি কোনো প্লাগইনের কাছে এত পুরোনো PluginFactory এর কোনো ইমপ্লিমেন্টেশন না থাকে, তবে এটি null রিটার্ন করতে পারে, সেক্ষেত্রে CarUi কম্পোনেন্টগুলোর স্ট্যাটিক্যালি-লিঙ্ক করা ইমপ্লিমেন্টেশন ব্যবহৃত হয়।

স্ট্যাটিক কার ইউআই লাইব্রেরির পুরোনো সংস্করণগুলোর সাথে ব্যাকওয়ার্ড কম্প্যাটিবিলিটি বজায় রাখার জন্য, আপনার প্লাগইনের PluginVersionProvider ক্লাসের ইমপ্লিমেন্টেশন থেকে maxVersion ২, ৫ এবং তার চেয়ে উচ্চতর সংস্করণ সমর্থন করার পরামর্শ দেওয়া হচ্ছে। সংস্করণ ১, ৩ এবং ৪ সমর্থিত নয়। আরও তথ্যের জন্য, PluginVersionProviderImpl দেখুন।

PluginFactory হলো সেই ইন্টারফেস যা CarUi-এর অন্য সব কম্পোনেন্ট তৈরি করে। এটি আরও নির্ধারণ করে যে তাদের ইন্টারফেসের কোন সংস্করণটি ব্যবহার করা হবে। যদি প্লাগইনটি এই কম্পোনেন্টগুলোর কোনোটি ইমপ্লিমেন্ট করতে না চায়, তবে সেগুলোর তৈরির ফাংশনে এটি null রিটার্ন করতে পারে (টুলবার ছাড়া, যার জন্য একটি আলাদা customizesBaseLayout() ফাংশন রয়েছে)।

pluginFactory নির্ধারণ করে দেয় যে CarUi কম্পোনেন্টগুলোর কোন সংস্করণগুলো একসাথে ব্যবহার করা যাবে। উদাহরণস্বরূপ, এমন কোনো pluginFactory থাকবে না যা একটি Toolbar এর সংস্করণ ১০০ এবং একটি RecyclerView এর সংস্করণ ১ উভয়ই তৈরি করতে পারে, কারণ সেক্ষেত্রে কম্পোনেন্টগুলোর বিভিন্ন সংস্করণ একসাথে কাজ করবে এমন কোনো নিশ্চয়তা থাকবে না। টুলবার সংস্করণ ১০০ ব্যবহার করার জন্য, ডেভেলপারদের এমন একটি pluginFactory এর ইমপ্লিমেন্টেশন প্রদান করতে হবে যা একটি টুলবার সংস্করণ ১০০ তৈরি করে, এবং এটিই অন্যান্য কম্পোনেন্টের সংস্করণ তৈরির বিকল্পগুলোকে সীমিত করে দেয়। অন্যান্য কম্পোনেন্টের সংস্করণগুলো সমান নাও হতে পারে, যেমন একটি pluginFactoryOEMV100 একটি ToolbarControllerOEMV100 এবং একটি RecyclerViewOEMV70 তৈরি করতে পারে।

টুলবার

ভিত্তি বিন্যাস

টুলবার এবং "বেস লেআউট" খুব ঘনিষ্ঠভাবে সম্পর্কিত, তাই যে ফাংশনটি টুলবার তৈরি করে তার নাম installBaseLayoutAroundবেস লেআউট হলো এমন একটি ধারণা যা টুলবারকে অ্যাপের কন্টেন্টের চারপাশে যেকোনো জায়গায় স্থাপন করার সুযোগ দেয়। এর ফলে অ্যাপের উপরে/নীচে, পাশ বরাবর উল্লম্বভাবে, বা এমনকি পুরো অ্যাপটিকে ঘিরে থাকা একটি বৃত্তাকার টুলবারও তৈরি করা যায়। টুলবার/বেস লেআউটটিকে ঘিরে রাখার জন্য installBaseLayoutAround ফাংশনে একটি ভিউ পাস করার মাধ্যমে এটি সম্পন্ন করা হয়।

প্লাগইনটি প্রদত্ত ভিউটি গ্রহণ করবে, সেটিকে তার প্যারেন্ট থেকে বিচ্ছিন্ন করবে, প্যারেন্টের একই ইন্ডেক্সে এবং সদ্য বিচ্ছিন্ন হওয়া ভিউটির মতো একই LayoutParams ব্যবহার করে প্লাগইনটির নিজস্ব লেআউট ইনফ্লেট করবে, এবং তারপর সদ্য ইনফ্লেট করা লেআউটটির ভেতরে কোথাও ভিউটিকে পুনরায় সংযুক্ত করবে। অ্যাপের অনুরোধে, ইনফ্লেট করা লেআউটটিতে টুলবার থাকবে।

অ্যাপটি টুলবার ছাড়া একটি বেস লেআউটের জন্য অনুরোধ করতে পারে। যদি এটি তা করে, তাহলে installBaseLayoutAround null রিটার্ন করা উচিত। বেশিরভাগ প্লাগইনের জন্য এটুকুই যথেষ্ট, কিন্তু যদি প্লাগইনের লেখক অ্যাপের কিনারা বরাবর কোনো ডেকোরেশন প্রয়োগ করতে চান, তবে সেটিও একটি বেস লেআউটের মাধ্যমে করা যেতে পারে। এই ডেকোরেশনগুলো বিশেষ করে আয়তক্ষেত্রাকার নয় এমন স্ক্রিনের ডিভাইসগুলোর জন্য উপযোগী, কারণ এগুলো অ্যাপটিকে একটি আয়তক্ষেত্রাকার স্থানে ঠেলে দিতে পারে এবং সেই অ-আয়তক্ষেত্রাকার স্থানে মসৃণ ট্রানজিশন যোগ করতে পারে।

installBaseLayoutAround একটি Consumer<InsetsOEMV1> ও পাস করা হয়। এই কনজিউমারটি অ্যাপকে এটা বোঝানোর জন্য ব্যবহার করা যেতে পারে যে, প্লাগইনটি অ্যাপের কন্টেন্টকে আংশিকভাবে ঢেকে রাখছে (টুলবার দিয়ে বা অন্য কোনোভাবে)। তখন অ্যাপটি বুঝতে পারবে যে এই ফাঁকা জায়গায় ড্রয়িং চালিয়ে যেতে হবে, কিন্তু ব্যবহারকারীর জন্য গুরুত্বপূর্ণ ও ইন্টারঅ্যাক্ট করার মতো কম্পোনেন্টগুলোকে এর বাইরে রাখতে হবে। আমাদের রেফারেন্স ডিজাইনে এই এফেক্টটি ব্যবহার করা হয়েছে, যাতে টুলবারটি আধা-স্বচ্ছ হয় এবং এর নিচে লিস্ট স্ক্রল করতে পারে। যদি এই ফিচারটি ইমপ্লিমেন্ট করা না হতো, তাহলে একটি লিস্টের প্রথম আইটেমটি টুলবারের নিচে আটকে থাকত এবং ক্লিক করা যেত না। যদি এই এফেক্টটির প্রয়োজন না হয়, তাহলে প্লাগইনটি কনজিউমারটিকে উপেক্ষা করতে পারে।

টুলবারের নিচে কন্টেন্ট স্ক্রলিং চিত্র ২. টুলবারের নিচে কন্টেন্টের স্ক্রলিং

অ্যাপের দৃষ্টিকোণ থেকে, যখন প্লাগইনটি নতুন ইনসেট পাঠায়, তখন এটি সেইসব অ্যাক্টিভিটি বা ফ্র্যাগমেন্ট থেকে সেগুলো গ্রহণ করে যেগুলো 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 হিন্ট দেওয়া হয়, যা নির্দেশ করে যে র‍্যাপ করা ভিউটি পুরো অ্যাপ জুড়ে থাকবে নাকি শুধু একটি ছোট অংশ জুড়ে থাকবে। এর মাধ্যমে প্রান্ত বরাবর এমন কিছু ডেকোরেশন প্রয়োগ করা এড়ানো যায়, যেগুলো কেবল পুরো স্ক্রিনের প্রান্ত বরাবর থাকলেই অর্থবহ হয়। নন-ফুলস্ক্রিন বেস লেআউট ব্যবহার করে এমন একটি নমুনা অ্যাপ হলো সেটিংস, যেখানে ডুয়াল-পেন লেআউটের প্রতিটি পেনের নিজস্ব টুলবার রয়েছে।

যেহেতু toolbarEnabled মান false হলে installBaseLayoutAround null রিটার্ন করার কথা, তাই প্লাগইনটি যে বেস লেআউট কাস্টমাইজ করতে চায় না, তা বোঝানোর জন্য customizesBaseLayout থেকে অবশ্যই false রিটার্ন করতে হবে।

রোটারি কন্ট্রোল সম্পূর্ণরূপে সমর্থন করার জন্য বেস লেআউটে অবশ্যই একটি FocusParkingView এবং একটি FocusArea থাকতে হবে। যেসব ডিভাইস রোটারি সমর্থন করে না, সেগুলোতে এই ভিউগুলো বাদ দেওয়া যেতে পারে। FocusParkingView/FocusAreas স্ট্যাটিক CarUi লাইব্রেরিতে ইমপ্লিমেন্ট করা আছে, তাই কনটেক্সট থেকে ভিউগুলো তৈরি করার জন্য ফ্যাক্টরি সরবরাহ করতে setRotaryFactories ব্যবহার করা হয়।

ফোকাস ভিউ তৈরি করতে ব্যবহৃত কনটেক্সট অবশ্যই সোর্স কনটেক্সট হতে হবে, প্লাগইনের কনটেক্সট নয়। FocusParkingView ট্রি-এর প্রথম ভিউ-এর যতটা সম্ভব কাছাকাছি হওয়া উচিত, কারণ যখন ব্যবহারকারীর কাছে কোনো ফোকাস দৃশ্যমান থাকার কথা নয়, তখন এটিই ফোকাসড থাকে। FocusArea যে একটি রোটারি নাজ জোন, তা বোঝানোর জন্য বেস লেআউটে টুলবারটিকে অবশ্যই এর মধ্যে রাখতে হবে। যদি FocusArea প্রদান করা না হয়, তবে ব্যবহারকারী রোটারি কন্ট্রোলার দিয়ে টুলবারের কোনো বাটনে নেভিগেট করতে পারবেন না।

টুলবার কন্ট্রোলার

ফেরত আসা প্রকৃত ToolbarController বেস লেআউটের চেয়ে বাস্তবায়ন করা অনেক বেশি সহজ হওয়া উচিত। এর কাজ হলো এর সেটারগুলোতে পাঠানো তথ্য গ্রহণ করে বেস লেআউটে তা প্রদর্শন করা। বেশিরভাগ মেথড সম্পর্কে তথ্যের জন্য Javadoc দেখুন। কিছু জটিল মেথড নিচে আলোচনা করা হলো।

getImeSearchInterface ব্যবহার করা হয় IME (কিবোর্ড) উইন্ডোতে সার্চের ফলাফল দেখানোর জন্য। এটি কিবোর্ডের পাশাপাশি সার্চের ফলাফল প্রদর্শন বা অ্যানিমেট করার জন্য উপযোগী হতে পারে, যেমন যদি কিবোর্ডটি স্ক্রিনের অর্ধেক জায়গা জুড়ে থাকে। এর বেশিরভাগ কার্যকারিতাই স্ট্যাটিক CarUi লাইব্রেরিতে প্রয়োগ করা হয়েছে; প্লাগইনের সার্চ ইন্টারফেসটি শুধু স্ট্যাটিক লাইব্রেরিকে TextView এবং onPrivateIMECommand কলব্যাকগুলো পাওয়ার জন্য মেথড সরবরাহ করে। এটি সমর্থন করার জন্য, প্লাগইনটির এমন একটি TextView সাবক্লাস ব্যবহার করা উচিত যা onPrivateIMECommand ওভাররাইড করে এবং প্রদত্ত লিসেনারে কলটিকে তার সার্চ বারের TextView হিসেবে পাস করে।

setMenuItems কেবল স্ক্রিনে MenuItems প্রদর্শন করে, কিন্তু এটি আশ্চর্যজনকভাবে প্রায়শই কল করা হয়। যেহেতু MenuItems-এর জন্য প্লাগইন API অপরিবর্তনীয়, তাই যখনই কোনো MenuItem পরিবর্তিত হয়, একটি সম্পূর্ণ নতুন setMenuItems কল করা হয়। এটি এমন একটি তুচ্ছ কারণেও ঘটতে পারে, যেমন একজন ব্যবহারকারী একটি সুইচ MenuItem-এ ক্লিক করেছেন এবং সেই ক্লিকের ফলে সুইচটি টগল হয়েছে। পারফরম্যান্স এবং অ্যানিমেশন উভয় কারণেই, পুরানো এবং নতুন MenuItems তালিকার মধ্যে পার্থক্য গণনা করার এবং শুধুমাত্র যে ভিউগুলো প্রকৃতপক্ষে পরিবর্তিত হয়েছে সেগুলো আপডেট করার পরামর্শ দেওয়া হয়। MenuItems একটি ' key ফিল্ড প্রদান করে যা এই কাজে সাহায্য করতে পারে, কারণ একই MenuItem-এর জন্য setMenuItems এর বিভিন্ন কলে 'key' একই থাকা উচিত।

প্রসঙ্গ

কনটেক্সট ব্যবহার করার সময় প্লাগইনটিকে অবশ্যই সতর্ক থাকতে হবে, কারণ প্লাগইন এবং "সোর্স" উভয় প্রকারের কনটেক্সট রয়েছে। প্লাগইন কনটেক্সটটি getPluginFactory এর একটি আর্গুমেন্ট হিসেবে দেওয়া হয়, এবং এটিই একমাত্র কনটেক্সট যার মধ্যে প্লাগইনের রিসোর্সগুলো থাকে। এর মানে হলো, প্লাগইনের মধ্যে লেআউট ইনফ্লেট করার জন্য শুধুমাত্র এই কনটেক্সটটিই ব্যবহার করা যায়।

তবে, প্লাগইন কনটেক্সটে সঠিক কনফিগারেশন সেট করা নাও থাকতে পারে। সঠিক কনফিগারেশন পাওয়ার জন্য, আমরা কম্পোনেন্ট তৈরি করার মেথডগুলোতে সোর্স কনটেক্সট সরবরাহ করি। সোর্স কনটেক্সট সাধারণত একটি অ্যাক্টিভিটি হয়, কিন্তু কিছু ক্ষেত্রে এটি একটি সার্ভিস বা অন্য কোনো অ্যান্ড্রয়েড কম্পোনেন্টও হতে পারে। প্লাগইন কনটেক্সটের রিসোর্সগুলোর সাথে সোর্স কনটেক্সটের কনফিগারেশন ব্যবহার করার জন্য, createConfigurationContext ব্যবহার করে একটি নতুন কনটেক্সট তৈরি করতে হবে। যদি সঠিক কনফিগারেশন ব্যবহার করা না হয়, তাহলে অ্যান্ড্রয়েড স্ট্রিক্ট মোড ভায়োলেশন ঘটবে এবং ইনফ্লেটেড ভিউগুলোর সঠিক ডাইমেনশন নাও থাকতে পারে।

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

মোড পরিবর্তন

কিছু প্লাগইন তাদের কম্পোনেন্টগুলোর জন্য একাধিক মোড সাপোর্ট করতে পারে, যেমন স্পোর্ট মোড বা ইকো মোড , যেগুলো দেখতে আলাদা। CarUi-তে এই ধরনের কার্যকারিতার জন্য কোনো বিল্ট-ইন সাপোর্ট নেই, কিন্তু প্লাগইনটিকে এটি সম্পূর্ণরূপে অভ্যন্তরীণভাবে প্রয়োগ করা থেকে আটকানোর মতো কিছু নেই। কখন মোড পরিবর্তন করতে হবে তা বোঝার জন্য প্লাগইনটি যেকোনো পরিস্থিতি পর্যবেক্ষণ করতে পারে, যেমন ব্রডকাস্ট শোনা। মোড পরিবর্তনের জন্য প্লাগইনটি কোনো কনফিগারেশন পরিবর্তন ঘটাতে পারে না, তবে কনফিগারেশন পরিবর্তনের উপর নির্ভর করার পরামর্শও দেওয়া হয় না, কারণ প্রতিটি কম্পোনেন্টের চেহারা ম্যানুয়ালি আপডেট করা ব্যবহারকারীর জন্য অনেক বেশি মসৃণ এবং এটি এমন ট্রানজিশনের সুযোগ দেয় যা কনফিগারেশন পরিবর্তনের মাধ্যমে সম্ভব নয়।

জেটপ্যাক কম্পোজ

Jetpack Compose ব্যবহার করে প্লাগইন তৈরি করা যায়, কিন্তু এটি একটি আলফা-স্তরের বৈশিষ্ট্য এবং এটিকে স্থিতিশীল বলে গণ্য করা উচিত নয়।

প্লাগইনগুলো রেন্ডার করার জন্য একটি কম্পোজ-সক্ষম সারফেস তৈরি করতে ComposeView ব্যবহার করতে পারে। কম্পোনেন্টগুলোর getView মেথড থেকে অ্যাপে এই ComposeView রিটার্ন করা হয়।

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