অ্যাপ ফাউন্ডেশন

Compose-এর মাধ্যমে প্রতিটি স্ক্রিনের জন্য আপনার অ্যাপ স্কেল করা

স্পিকার

  • ফ্রান্সেসকো রোমানো

    সিনিয়র ডেভেলপার রিলেশনস ইঞ্জিনিয়ার

সম্পর্কে

টক সম্পর্কে

Android ইকোসিস্টেম অনেক বড়, যেখানে ব্যবহারকারীরা ফোন, ট্যাবলেট, ফোল্ডেবল ও ডেস্কটপ এনভায়রনমেন্টের মধ্যে নির্বিঘ্নে পাল্টাতে পারেন। এখন আমরা Compose First, এই সেশনটি এক্সপ্লোর করে যে কীভাবে আপনার অ্যাপকে রিসাইজ করা যায় এমন উইন্ডো এবং বিভিন্ন অ্যাস্পেক্ট রেশিওর জন্য আর্কিটেক্ট করা যায়—যেখানে প্রথাগত লেআউট সংক্রান্ত অনুমান ভেঙে যায়, যার ফলে UI স্ট্রেচ হয়ে যায় এবং স্টেট হারিয়ে যায়।

Jetpack Compose, Navigation 3 এবং WindowManager-এর লেটেস্ট ক্ষমতাগুলি কীভাবে আপনাকে ডায়নামিক লেআউট তৈরি করতে সাহায্য করতে পারে তা এক্সপ্লোর করুন যা স্বাভাবিকভাবেই কমপ্যাক্ট স্ক্রিন থেকে ডুয়াল-পেন ডিসপ্লেতে স্কেল করে। Gemini এবং আমাদের স্কিল, যেমন অ্যাডাপ্টিভ স্কিল ব্যবহার করে আপনার ডেভেলপমেন্ট ওয়ার্কফ্লোকে ত্বরান্বিত করার কৌশলগুলি জেনে নিন, যাতে বড় স্ক্রিনের জন্য লিগ্যাসি কোডবেস অটোমেটিক রিফ্যাক্টর করা যায়।

সারাংশ

  • Android প্ল্যাটফর্মে অ্যাডাপ্টিভ অ্যাপের প্রয়োজন হয় কারণ Android 16 থেকে শুরু করে বড় স্ক্রিনে সিস্টেম ওরিয়েন্টেশন ও অ্যাস্পেক্ট রেশিও লক উপেক্ষা করে, Android 17-এ ম্যানিফেস্ট অপ্ট-আউট সরিয়ে দেওয়া হয়েছে।
  • পরীক্ষামূলক MediaQuery API, উইন্ডোর সাইজ ও ফোল্ডিং পোস্টারের মতো অ্যাক্টিভ UI ক্ষমতা ডিক্লেরেটিভভাবে ট্র্যাক করে। এটি কম্পোজ স্টেটে একাধিক লিগ্যাসি প্ল্যাটফর্ম ম্যানেজারকে একসাথে সেলাই করার পরিবর্তে কাজ করে।
  • স্টেবল Grid এবং FlexBox প্রিমিটিভ অ্যাডাপ্টিভ লেআউট বিল্ডিং ব্লক প্রদান করে। Grid একটি কম্পোজেবলের মধ্যে প্যারামিটারাইজড মাল্টি-ডাইমেনশনাল অ্যারেঞ্জমেন্টের সুবিধা দেয়, যেখানে FlexBox ওয়ান-ডাইমেনশনাল স্পেস ডিস্ট্রিবিউশন ও র‍্যাপিংয়ের সুবিধা দেয়।
  • Jetpack Navigation 3 স্টেট ও দৃশ্যের চারপাশে অ্যাপ-লেভেল মাল্টি-পেন ফ্লো স্ট্রাকচার করে। কাস্টম সিন স্ট্র্যাটেজি UI ক্ষমতা এবং উইন্ডো সাইজ ক্লাস মূল্যায়ন করে যাতে সিঙ্গেল-পেন এবং মাল্টি-পেন প্রেজেন্টেশনের মধ্যে ডাইনামিকভাবে পরিবর্তন করা যায়।
  • Gemini-র জন্য কম্পোজ অ্যাডাপ্টিভ স্কিল, অ্যাপ মাইগ্রেশনকে আধুনিক রেসপন্সিভ কম্পোনেন্টে অটোমেট করে। আপডেট করা স্কিল লেআউট রিফ্যাক্টর করে, স্টেট লস সংক্রান্ত সমস্যার সমাধান করে এবং কীবোর্ড শর্টকাট ও কার্সরের মতো ডেস্কটপ ইনপুট কনফিগার করে।

রিসোর্স

Gemini Notebook

বিষয়

  • jetpack-compose
  • অ্যাডাপ্টিভ-লেআউট
  • মিডিয়াক্যোয়েরি
  • গ্রিড-লেআউট
  • flexbox
  • navigation-3
  • android-skills

ট্রান্সস্ক্রিপ্ট করা

স্ক্রিন জুড়ে অ্যাপ স্কেল করার ভূমিকা

  • ফ্রান্সেসকো রোমানো

    হ্যালো।

  • আজ, আমরা Jetpack Compose-এর মাধ্যমে কীভাবে আপনার অ্যাপকে প্রতিটি স্ক্রিনে স্কেল করা যায় সে সম্পর্কে একটি গভীর প্রযুক্তিগত আলোচনা করতে যাচ্ছি।

  • সব স্ক্রিনে স্কেলিং কেন?

  • কারণ Android ইকোসিস্টেম বিভিন্ন ফর্ম ফ্যাক্টর জুড়ে প্রসারিত হয়েছে।

  • আপনার APK ফোন, ফোল্ডেবল, ট্যাবলেট, Chromebook ও Googlebook, টিভি, অটোমোটিভ ইনফোটেনমেন্ট সিস্টেম, XR ডিভাইস এবং আরও অনেক কিছুতে চলতে পারে।

  • তাই প্রথমত, আপনার ব্যবহারকারীরা যেখানে আছেন সেখানে তাদের সাথে দেখা করুন।

  • দ্বিতীয়ত, অ্যাডাপ্টিভ অ্যাপের দিকে Android প্ল্যাটফর্মের শিফ্ট, এটি ক্ষণস্থায়ী নয়।

  • আমরা যে শুধু ফোন ব্যবহার করার যুগে ফিরে যাচ্ছি তা নয়, এটি এখন থেকে যাবে।

  • আর আমার কাছে একটি দ্রুত টাইমলাইন আছে।

  • ২০২২ সালে আমরা ভিত্তিপ্রস্তর স্থাপন করেছি।

  • আমাদের কাছে Android 12L, Jetpack WindowManager-এর প্রথম রিলিজ ছিল, আমরা আপনাকে উইন্ডো সাইজ ক্লাসের মতো API দিয়েছিলাম এবং আমরা অ্যাডাপ্টিভ অ্যাপ কোয়ালিটি সংক্রান্ত নির্দেশিকার প্রথম ভার্সন প্রকাশ করেছিলাম।

  • তারপর, আমরা Google Play-ও আপডেট করেছি যাতে ডিভাইসের ধরন অনুযায়ী রেটিং ভাগ করা যায় এবং আমরা র‍্যাংকিং সংক্রান্ত সিদ্ধান্তে অ্যাডাপ্টিভ অ্যাপ কোয়ালিটি নির্দেশিকা অন্তর্ভুক্ত করা শুরু করেছি।

  • Android 15-এ, আমরা AOSP-তে ডেস্কটপ উইন্ডোইং এবং Compose Material 3 অ্যাডাপ্টিভ নিয়ে এসেছি যা এখন স্টেবল।

  • তারপর Android 16, SDK 36 থেকে শুরু করে, সিস্টেম বড় স্ক্রিনে ওরিয়েন্টেশন লক, অ্যাস্পেক্ট রেশিও লক, রিসাইজেবিলিটি ফ্ল্যাগকে উপেক্ষা করে।

  • কানেক্ট করা ডিসপ্লে রিলিজ করার সাথে সাথে, এখন আপনার APK কার্যত যেকোনও উইন্ডো সাইজে রান করতে পারে।

  • এছাড়াও, SDK 36 আপনাকে ওরিয়েন্টেশন ও অ্যাস্পেক্ট রেশিও ওভাররাইড থেকে অপ্ট-আউট করার জন্য একটি সাময়িক ম্যানিফেস্ট ফ্ল্যাগ দেয়।

  • Android 17-এ, অপ্ট-আউট করার বিকল্পটি আর নেই।

  • তাই, অ্যাডাপ্টিভ বিল্ডিং হল বেসলাইন প্ল্যাটফর্মের প্রয়োজনীয়তা।

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

  • আপনার কিছু স্টেট লস, ফ্র্যাগমেন্টেড উইন্ডো API, লেআউট কখন পরিবর্তন করতে হবে সেই সম্পর্কে অস্পষ্ট সিগন্যাল এবং কোডবেসে কিছু পুরনো অনুমান রয়েছে।

  • তাই, ডেভেলপারদের চ্যালেঞ্জের এই দশক থেকে আমরা যা শিখেছি তা সরাসরি দুটি জায়গায় প্রয়োগ করেছি: প্রথমত, UI টুলকিটে।

  • আজ, আমরা Jetpack Compose-এ কিছু প্রধান নতুন ক্ষমতা এক্সপ্লোর করব: মিডিয়া কোয়েরি, গ্রিড, FlexBox এবং Navigation 3।

  • তবে, Android Studio-তে Gemini-র জন্য অফিসিয়াল Android স্কিল এবং Android CLI-এর মাধ্যমে আপনার সাথে কোড লেখে এমন এজেন্টিক টুলের ক্ষেত্রেও এটি প্রযোজ্য।

  • যেমন, আমরা Compose অ্যাডাপ্টিভ স্কিল দেখব যা এই বছরের শুরুতে Google I/O-তে রিলিজ করা হয়েছে।

  • এটি আগে থেকেই ফোনের লেআউটকে আরও বড় উইন্ডো সাইজে মাইগ্রেট করতে পারত।

  • এখন, এটি আপনার স্টেট লসও ঠিক করতে পারে, এটি টেবিলটপের মতো ফোল্ডেবল-অনন্য অভিজ্ঞতা প্রয়োগ করতে পারে এবং কীবোর্ড শর্টকাট, কাস্টম কার্সর, টুলটিপ ইত্যাদি আবিষ্কার ও প্রয়োগ করে ইনপুট সাপোর্ট উন্নত করতে পারে।

  • ঠিক আছে, Jetpack Compose দিয়ে শুরু করা যাক, যা এই সবকিছুর ভিত্তি।

  • Jetpack Compose ২০২১ সালে স্টেবল ভার্সন হিসেবে রিলিজ করা হয়, তাই এটি তার পঞ্চম জন্মদিন।

  • এবং এই পাঁচ বছর ধরে, আমরা হাজার হাজার প্রোডাকশন অ্যাপ জুড়ে দেখেছি যে Compose-এর মাধ্যমে অ্যাডাপ্টিভ তৈরি করা কতটা সহজ এবং আরও নির্ভরযোগ্য।

  • Android ডেভেলপমেন্ট এখন আনুষ্ঠানিকভাবে কম্পোজ-ফার্স্ট।

  • এর অর্থ হল, Compose-এর জন্য সব নতুন আধুনিক অ্যাডাপ্টিভ লেআউট, স্টাইলিং, নেভিগেশন কম্পোনেন্ট তৈরি করা হয়েছে।

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

  • এবং এটি ব্যাখ্যা করার জন্য, আমি ভিউ সিস্টেমের সাথে একটি দ্রুত তুলনা করতে চাই।

  • তাই, ভিউ সিস্টেম কীভাবে কাজ করে তা ভেবে দেখুন।

  • আপনার UI ছিল পরিবর্তনযোগ্য অবজেক্টের একটি ট্রি যা আপনি স্ট্যাটিক XML ফাইল থেকে একবার ইনফ্লেট করেছিলেন।

  • আপনার কাছে বিভিন্ন রিসোর্স কোয়ালিফায়ার ছিল এবং আসুন এখানে প্রধান সমস্যাটি দিয়ে শুরু করা যাক: বিভিন্ন রিসোর্স কোয়ালিফায়ার জুড়ে ডুপ্লিকেশন।

  • XML-এ অ্যাডাপ্টিভ UI তৈরি করার সময়, লেআউট ফাইলের মধ্যে কোনও শর্তাধীন লজিক থাকে না।

  • পরিবর্তে, আপনি রিসোর্স ফোল্ডার জুড়ে সম্পূর্ণ হায়ারার্কি ডুপ্লিকেট করেন, যেমন লেআউট, লেআউট সবচেয়ে ছোট উইন্ডো ৬০০ ডিপি, লেআউট ল্যান্ড ইত্যাদি।

  • ঠিক আছে, ডানদিকে EditText-এর উদাহরণ দেখুন।

    <LinearLayout android:orientation="vertical">
      <EditText
          android:id="@+id/text_msg"
          android:inputType="textCapCharacters"
          android:maxLength="12" />
    </LinearLayout>
    <ConstraintLayout>
      <TextInputEditText
        android:id="@+id/text_input"
        android:inputType="text" />
    </ConstraintLayout>
  • এটি হল ঠিক একই লজিক্যাল কম্পোনেন্ট যা টেক্সট ইনপুট ম্যানেজ করে এবং এটি বিভিন্ন XML ফাইল জুড়ে ছড়িয়ে থাকে।

  • আপনি যদি ফোন লেআউটে inputType বা maxLength-এর মতো অ্যাট্রিবিউট আপডেট করেন, তাহলে কম্পাইলার আপনাকে ট্যাবলেট লেআউটেও এটি আপডেট করতে বাধ্য করে না।

  • এবং যেহেতু কম্পোনেন্টগুলি বিভিন্ন ফাইল জুড়ে ডিকাপল করা হয়, তাই সময়ের সাথে সাথে স্ট্রাকচারাল ড্রিফ্ট প্রায় নিশ্চিত।

  • এবং সেই ডুপ্লিকেশন অন্য একটি সমস্যার সাথেও লিঙ্ক করা আছে, যা কনফিগারেশন পরিবর্তনের সময় স্টেট লস।

  • মনে রাখবেন যে ভিউ সিস্টেমে UI কম্পোনেন্ট স্টেটফুল হয়।

  • একটি EditText-এর নিজস্ব ইন্টার্নাল এডিট করা যায় এমন বাফার, কাস্টম কার্সর পজিশন, ফোকাস স্টেট, কিছু স্টেট থাকে।

  • তাই, ধরা যাক, কোনও ব্যবহারকারী ডিভাইস আনফোল্ড করলে বা উইন্ডো ছোট বড় করে নির্দিষ্ট থ্রেশহোল্ডে নিয়ে গেলে, Android সম্পূর্ণ ভিউ হায়ারার্কি নষ্ট করে দেয় এবং নতুন XML ফাইল প্রথম থেকে ইনফ্লেট করে।

  • ব্যবহারকারীর ইনপুট সেভ করার জন্য, ফ্রেমওয়ার্ক onSaveInstanceState-এর উপর নির্ভর করে, যা সেই ভিউয়ের আইডির সাথে লিঙ্ক করা স্টেট প্যাক করবে।

  • কিন্তু এর ফলে তিনটি সম্ভাব্য ব্যর্থতার মোড তৈরি হয়।

  • প্রথমত, অন্য XML-এ কেউ যদি ভুলবশত ID-র নাম text_msg-এর পরিবর্তে text_input রাখেন, তাহলে স্টেট রিস্টোরেশন কোনও সতর্কতা ছাড়াই ব্যবহারকারীর ইনপুট টেক্সট ড্রপ করে দেয়।

  • দ্বিতীয়ত, onViewCreated-এর পরে onRestoreInstanceState রান করে, যা প্রায়শই ViewModel অবজারভারের সাথে সংঘর্ষে লিপ্ত হয় এবং এর ফলে কলব্যাক লুপ তৈরি হতে পারে।

  • এবং তৃতীয়ত, আপনি যদি কোয়ালিফায়ার জুড়ে উইজেটের ধরন অদলবদল করেন, ধরা যাক, একটি কম্প্যাক্ট স্ক্রিনে আপনি হয়ত স্পিনার চান এবং একটি চওড়া স্ক্রিনে আপনি RadioGroup চান, যদি তারা একই আইডি শেয়ার করে, তাহলে রানটাইমে ClassCastException-এর কারণে সেটি ক্র্যাশ করে।

  • তবে, Jetpack Compose-এ আপনার UI এমন কোনও অবজেক্ট নয় যা আপনি একবার তৈরি করেন।

  • আপনার UI হল স্টেটের একটি পিওর ফাংশন।

  • এবং এটি কনফিগারেশন পরিবর্তনকে সম্পূর্ণভাবে বদলে দেয়।

  • Compose-এ কনফিগারেশন পরিবর্তন বলতে কী বোঝায়?

  • আমার কাছে একটি ইঙ্গিত আছে।

কনফিগারেশন পরিবর্তন ও UI ক্ষমতা ম্যানেজ করা

  • ফ্রান্সেসকো রোমানো

    হ্যাঁ।

  • Compose-এ, কনফিগারেশন পরিবর্তন হল আরেকটি রিকম্পোজিশন এবং রিকম্পোজিশন সবসময়ই হয়।

  • উইন্ডোটি আরও চওড়া হলে, সম্ভবত ব্যবহারকারী ডিভাইসটি আনফোল্ড করার কারণে, আপনার কম্পোজেবল নতুন সাইজটি পড়ে এবং যার আউটপুট এটির উপর নির্ভর করে সেটি আবার রান করে।

  • এটিই এটিই পুরো ইভেন্ট।

  • তবে এখন, লেটেস্ট কম্পোজ টুল ও কম্পোনেন্ট ব্যবহার করে আধুনিক অ্যাডাপ্টিভ অ্যাপ লেখার অর্থ কী তা দেখা যাক।

  • তাই, XML-এর দুনিয়ায় আপনাকে এই ধরনের প্রশ্ন করতে হয়, "আমি কি ফোন, ট্যাবলেট নাকি ডেস্কটপে রান করছি?" এবং তারপর ডিভাইসের ক্যাটেগরির উপর ভিত্তি করে UI-এর ব্রাঞ্চিং করতে হয়।

  • তবে, এটি না করার একটি মৌলিক কারণ আছে।

  • এমনকি একই ফিজিক্যাল ডিভাইসে রান করার সময়ও, রানটাইমে অ্যাপ্লিকেশন কন্টেক্সট এবং UI ক্ষমতা ডাইনামিকভাবে পরিবর্তিত হতে পারে।

  • তাই, আমি যে মেসেজটি দিতে চাই তা হল UI সংক্রান্ত সিদ্ধান্ত বর্তমান অ্যাক্টিভ ক্ষমতার দ্বারা পরিচালিত হওয়া উচিত: বর্তমান উইন্ডোর সাইজ, ভঙ্গি, কিছু হার্ডওয়্যার বৈশিষ্ট্য।

  • তাই, আসুন একটি ক্লাসিক উদাহরণ দেখি, যা হল এক্সটার্নাল মনিটরে ডেস্কটপ উইন্ডোয়িং।

  • আপনি যদি নিজের Pixel ফোনটি নিয়ে আপনার ডেস্কে থাকা এক্সটার্নাল মনিটরে প্লাগ-ইন করেন, তাহলে ৬-ইঞ্চি পোর্ট্রেট স্ক্রিনে যে অ্যাপ প্রসেসটি চলছিল, সেটিই এখন ফিজিক্যাল কীবোর্ড এবং মাউস অ্যাটাচ করা ৩২-ইঞ্চি চওড়া মনিটরে চলবে।

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

  • এছাড়াও, Android-এ একাধিক উইন্ডো কাজ করে, তাই উইন্ডোর সাইজ ছোট-বড় করা যায়।

  • Googlebook-এর স্ক্রিন ফোনের থেকে বড় হলেও, আপনার অ্যাপটি ৪০০ ডিপি চওড়া উইন্ডোতে অন্যান্য অ্যাপের সাথে চলতে পারে।

  • তাই, স্ট্যাটিক ফিজিক্যাল ডিসপ্লে মেট্রিক্স কোয়েরি করাও অর্থহীন হতে পারে।

  • তাই, Compose-এর জন্য প্রথমে অ্যাপ আর্কিটেক্ট করতে, আমাদের বুঝতে হবে যে কীভাবে এনভায়রনমেন্ট কোয়েরি করতে হয়।

  • এবং আমি UI ক্ষমতার ধারণাকে আনুষ্ঠানিক করার চেষ্টা করে শুরু করতে চাই।

  • UI-এর ক্ষমতার মধ্যে শুধু উইন্ডোর প্রস্থ ও উচ্চতা বা ফোল্ডেবল পশ্চারই পড়ে না, তার সাথে বর্তমানে অ্যাক্টিভ ইনপুট ও আউটপুট মোডালিটিও পড়ে।

  • আমরা দেখেছি যে ডেস্কটপ এনভায়রনমেন্টে ব্যবহারকারীরা সবসময় টাচস্ক্রিন ব্যবহার করেন না।

  • তারা কীবোর্ড ও মাউস ব্যবহার করতে পারবে।

  • অটোমোটিভ সিস্টেমে, কিছু গাড়িতে রোটারি নব বা আলাদা ইনপুট, ইনপুট সিগন্যালের আলাদা পদ্ধতি থাকে।

  • এছাড়াও, স্ক্রিনের সাইজ ও ইনপুট ছাড়াও, ব্যবহারকারী ডিসপ্লে থেকে কত দূরে আছেন, যেমন দেখার দূরত্ব বা হার্ডওয়্যার ক্যামেরা বা মাইক্রোফোন বর্তমানে কানেক্ট করা আছে কিনা।

  • এগুলিই হল UI-এর সব ক্ষমতা।

mediaQuery API-এর মাধ্যমে পোস্টার শনাক্ত করা

  • ফ্রান্সেসকো রোমানো

    এখন, আপনি যদি সব সিগন্যাল ফিরিয়ে আনতে চান, তাহলে আপনাকে বিভিন্ন সোর্স থেকে ৫, ৬টি আলাদা API একসাথে জুড়তে হবে।

  • আপনার কনফিগারেশন অবজেক্ট, WindowMetricsCalculator, WindowInfoTracker, InputManager, PackageManager এবং তারপর ম্যানুয়ালি, এগুলির প্রত্যেকটিকে একটি স্টেট, একটি Compose স্টেটে কনভার্ট করতে হবে।

  • আপনি কি এটা করতে পছন্দ করেছেন?

  • আমি করিনি।

  • এই সমস্যার সমাধান করতে, আমরা androidx.compose.ui-এ MediaQuery API নিয়ে এসেছি।

    mediaQuery { ... }
    derivedMediaQuery { ... }
  • এটি CSS মিডিয়া কোয়েরি থেকে অনুপ্রাণিত।

  • আর মিডিয়া বলতে অডিও বা ভিডিও প্লেব্যাক বোঝায় না।

  • এটি সেই মাধ্যম বা পরিবেশকে বোঝায় যেখানে আপনার UI রেন্ডার করা হচ্ছে।

  • এবং MediaQuery আপনাকে UI ক্ষমতাগুলি কোয়েরি করার জন্য একটি একক ঘোষণামূলক কম্পোজ-নেটিভ উপায় দেয়।

  • UiMediaScope-এ দেখানো প্রপার্টিগুলি দেখা যাক।

    sealed interface UiMediaScope {
        @get:FrequentlyChangingValue val windowWidth: Dp
        @get:FrequentlyChangingValue val windowHeight: Dp
    
        val windowPosture: Posture // Flat, Tabletop, Book
        val pointerPrecision: PointerPrecision // Fine, Coarse, Blunt, None
        val keyboardKind: KeyboardKind // Physical, Virtual, None
    
        val hasCamera: Boolean
        val hasMicrophone: Boolean
        val viewingDistance: ViewingDistance // Near, Medium, Far
    }
  • ডিপি-তে windowWidth এবং windowHeight।

  • windowPosture, যা বর্তমানে ফোল্ড করা যায় এমন ডিভাইসের জন্য এবং ফ্ল্যাট, টেবিলটপ বা বুক হতে পারে।

  • pointerPrecision আপনাকে প্রাইমারি পয়েন্ট ডিভাইস কতটা নিখুঁত তা জানায়: মাউস, স্টাইলাস বা ট্র্যাকপ্যাডের জন্য ফাইন; টাচের জন্য কোর্স; জয়স্টিক, কন্ট্রোলার বা কোনওটিই না-এর জন্য ব্লান্ট।

  • keyboardKind আসল, ভার্চুয়াল বা কোনওটিই না হতে পারে।

  • hasCamera এবং hasMicrophone ডাইনামিকভাবে ট্র্যাক করে যে এই হার্ডওয়্যার উপলভ্য আছে কিনা।

  • প্লাগ-ইন করা যায় এমন এক্সটার্নাল ক্যামেরার কথা ভাবুন।

  • এবং viewingDistance-এর রিপোর্ট অনুযায়ী, কাছে, মাঝারি বা দূরে।

  • তবে আমার মনে হয়, আমরা যদি একটি বাস্তব উদাহরণ দেখি, তাহলে এটি বোঝা আরও সহজ হবে। উদাহরণটি হল, ডিভাইসটি টেবিলটপ মোডে আছে কিনা তা শনাক্ত করা।

  • টেবিলটপ মোড হল এমন একটি পসচার যা ব্যবহারকারীদের ডিভাইসটি অর্ধেক ভাঁজ করতে এবং সাধারণত কোনও সারফেসে সেট করতে দেয়।

  • এছাড়াও, এটি দীর্ঘ সময় ধরে ভিডিও প্লেব্যাক, ভিডিও কল, টাইম-ল্যাপ্স ফটোগ্রাফির জন্য দারুণ।

  • এছাড়াও, এটি রেসিপি ফলো করা বা স্লাইডশো দেখার জন্য খুবই উপযোগী।

  • এখন, আপনি যদি MediaQuery-এর আগে টেবিলটপ পশ্চার শনাক্ত করতে চান, তাহলে আপনাকে এই সমস্ত কোড লিখতে হবে।

    @Composable
    fun currentVideoPlayerLayout(): VideoPlayerLayout {
       val context = LocalContext.current
       val windowLayoutInfo by WindowInfoTracker.getOrCreate(context).windowLayoutInfo(context)
           .collectAsStateWithLifecycle(null)
       val isTabletopPosture = windowLayoutInfo?.displayFeatures?.any { displayFeature ->
           displayFeature is FoldingFeature &&
                   displayFeature.state == FoldingFeature.State.HALF_OPENED &&
                   displayFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
       } ?: false
       return if (isTabletopPosture) {
           VideoPlayerLayout.Tabletop
       } else {
           VideoPlayerLayout.Default
       }
    }
  • আপনাকে কনটেক্সট পেতে হবে, WindowInfoTracker-এর ইনস্ট্যান্স পেতে হবে, windowLayoutInfo সংগ্রহ করতে হবে, যা হল একটি ফ্লো, লাইফসাইকেল সম্পর্কে সচেতন কম্পোজ স্টেট এবং তারপর displayFeatures-এর মাধ্যমে ইটারেট করে দেখতে হবে যে এমন কোনও FoldingFeature আছে কিনা যা অর্ধেক খোলা এবং অনুভূমিক।

  • এবং এটি শুধুমাত্র ভঙ্গি শনাক্ত করার জন্য।

  • এটি অনেক বেশি বয়লারপ্লেট।

  • নতুন MediaQuery API-এর মাধ্যমে, এই সমস্ত কিছু মাত্র কয়েক লাইনের কোডে সঙ্কুচিত হয়ে যায়।

    @Composable
    fun currentVideoPlayerLayout(): VideoPlayerLayout {
       return mediaQuery {
           when(windowPosture) {
               UiMediaScope.Posture.Tabletop -> VideoPlayerLayout.Tabletop
               else -> VideoPlayerLayout.Default
           }
       }
    }
  • mediaQuery ফাংশনটি একটি ল্যাম্বডা প্যারামিটার নেয় এবং সেই ল্যাম্বডার রিসিভার স্কোপের মধ্যে, এটি আমরা এইমাত্র যে প্রপার্টির সেটটি দেখেছি তা প্রকাশ করে।

  • এই উদাহরণে, when এক্সপ্রেশনে শুধু windowPosture চেক করে, আমরা অবিলম্বে সঠিক কম্পোজেবল রিটার্ন করতে পারি।

  • API কীভাবে ব্যবহার করতে হয় তার আরও দুটি উদাহরণ এখানে দেওয়া হল।

    @Composable
    fun AdaptiveChatControls(
        onStartVideoCall: () -> Unit,
        onSendMessage: () -> Unit,
    ) {
        // 1. Return a Dp value directly based on pointer precision
        val chipPadding = mediaQuery {
            if (pointerPrecision == PointerPrecision.Fine) 2.dp else 8.dp
        }
    
        // 2. Return a Boolean combining hardware capabilities
        val canVideoCall = mediaQuery { hasCamera && hasMicrophone }
        val hasPhysicalKeyboard = mediaQuery {
            keyboardKind == KeyboardKind.Physical
        }
    
        Row(modifier = Modifier.padding(chipPadding)) {
            if (canVideoCall) {
                IconButton(onClick = onStartVideoCall) {
                    Icon(Icons.Default.Videocam, contentDescription = "Video call")
                }
            }
            Button(onClick = onSendMessage) {
                Text(if (hasPhysicalKeyboard) "Send (Ctrl+Enter)" else "Send")
            }
        }
    }
  • প্রথমটি হল তথ্যের গভীরতা।

  • তাই, কোনও ব্যবহারকারী ল্যাপটপ বা মাউস সহ ডেস্কটপ মনিটর ব্যবহার করলে এবং পয়েন্টার প্রিসিশন ফাইন হলে, আমরা UI কম্পোনেন্টের প্যাডিং আরও ছোট করতে পারি। এই উদাহরণে, 8 Dp থেকে 2 Dp করা যেতে পারে যাতে আরও বেশি কন্টেন্ট সহজে দেখানো যায়। যেহেতু ব্যবহারকারী আঙুল ব্যবহার করছেন না, তাই টাচ টার্গেট ছোট করা যেতে পারে।

  • এছাড়াও, ক্ষমতা-চালিত অ্যাকশন।

  • ধরা যাক, আমাদের কাছে একটি চ্যাট ইনপুট বার আছে, আমরা ভিডিও কল অ্যাকশন বোতামটি ডাইনামিকভাবে দেখাতে বা লুকাতে hasCamera এবং hasMicrophone চেক করতে পারি, অথবা আমরা কীবোর্ডের ধরন ফিজিক্যাল কিনা তা একত্রিত করে দেখাতে পারি, অথবা কিছু কীবোর্ড শর্টকাট ইঙ্গিত যেমন পাঠাতে Ctrl+Enter প্রেস করুন, তা দেখাতে বা লুকাতে পারি।

  • রান টাইমে এগুলি সব ফিরিয়ে আনা হয়।

  • এছাড়াও, MediaQuery টেস্টিং ও প্রিভিউ জেনারেশনের জন্য দারুণ।

    @Preview(name = "Tabletop + Fine Pointer", widthDp = 840, heightDp = 700)
    @Composable
    fun TabletopPlayerPreview() {
        val simulatedMediaScope = object : UiMediaScope {
            override val windowWidth = 840.dp
            override val windowHeight = 700.dp
            override val windowPosture = UiMediaScope.Posture.Tabletop
            override val pointerPrecision = UiMediaScope.PointerPrecision.Fine
            override val keyboardKind = UiMediaScope.KeyboardKind.Physical
            override val hasCamera = true
            override val hasMicrophone = true
            override val viewingDistance = UiMediaScope.ViewingDistance.Medium
        }
    
        CompositionLocalProvider(LocalUiMediaScope provides simulatedMediaScope) {
            VideoPlayerScreen()
        }
    }
  • তাই, মূলত, MediaQuery LocalUiMediaScope নামক কম্পোজিশন লোকালের মাধ্যমে এর রিসিভার সমাধান করে।

  • তাই, আপনি প্রিভিউতে CompositionLocalProvider-এর মাধ্যমে সহজেই একটি নকল UiMediaScope দিতে পারেন এবং কম্পোজ UI টেস্ট চালানোর জন্য আপনাকে ফোল্ডেবল ডিভাইস চালু করতে এবং ৯০° কোণে রাখতে বা মাউস অ্যাটাচ করতে হবে না।

Grid ও FlexBox-এর মাধ্যমে রেসপন্সিভ স্ক্রিন লেআউট

  • ফ্রান্সেসকো রোমানো

    ঠিক আছে, এখন আমরা জানি কীভাবে UI ক্ষমতা কোয়েরি করতে হয়।

  • এখন, আসুন দেখা যাক কীভাবে আসল লেআউট তৈরি করা যায়।

  • তার জন্য, আমি গ্রিড সম্পর্কে বলতে চাই, যা androidx.compose.foundation.layout-এ একটি নতুন লেআউট প্রিমিটিভ, যা এখন স্টেবল।

    Grid(
        modifier = Modifier.fillMaxSize().padding(16.dp),
        config = {
            val maxWidthDp = constraints.maxWidth.toDp()
            val columnCount = if (maxWidthDp < 600.dp) 2 else 4
    
            repeat(columnCount) {
                column(1.fr)
            }
            row(GridTrackSize.Auto)
            row(1.fr)
    
            gap(row = 16.dp, column = 8.dp)
            flow = GridFlow.Row
        }
    ) {
        // GridScope children...
    }
  • গ্রিড আপনাকে কলাম এবং সারিতে আইটেমগুলি প্রদর্শন করে একটি দ্বি-মাত্রিক লেআউট প্রয়োগ করতে দেয় যা উপলভ্য কন্টেনারের সাইজের সাথে মানানসই হয়।

  • কনফিগারেশন ব্লকে আপনি কী কী করতে পারবেন তা দেখে নেওয়া যাক।

  • আপনি কলাম বা সারি কল করে নতুন এলিমেন্ট বা ট্র্যাক যোগ করতে পারেন এবং রিপিটের মতো স্ট্যান্ডার্ড Kotlin লুপ ব্যবহার করতে পারেন।

  • আপনি ট্র্যাকের মধ্যে গাটার সেট করতে পারবেন।

  • আপনি ফ্লো-- GridFlow.Row বা GridFlow.Column-এর মাধ্যমে অটো-প্লেসমেন্টের দিক নিয়ন্ত্রণ করতে পারবেন।

  • এবং এখানে একটি মূল স্থাপত্যের বিবরণ রয়েছে।

  • কনফিগারেশন ব্লক পরিমাপ পাসের সময় চলে এবং গ্রিড কনফিগারেশন স্কোপ ঘনত্ব প্রয়োগ করে এবং সীমাবদ্ধতাগুলি প্রকাশ করে।

  • এর অর্থ হল, আপনি কনফিগারেশনের মধ্যে থেকেই constraints.maxWidth.toDp() পরীক্ষা করতে পারবেন এবং কোনও অতিরিক্ত রিকম্পোজিশন পাস ছাড়াই ট্র্যাকগুলি গণনা করতে পারবেন।

  • গ্রিডে, আপনি যখন চিলড্রেন যোগ করেন-- যখন আপনি মডিফায়ার ছাড়া চাইল্ড যোগ করেন, এটি অটোমেটিক পরবর্তী সেলে চলে যাবে।

    Grid(
        config = {
            repeat(3) { column(1.fr) }
            repeat(3) { row(100.dp) }
            gap(8.dp)
        }
    ) {
        // Spans 2x2 area (rows 1..2, columns 1..2)
        HeroCard(modifier = Modifier.gridItem(rows = 1..2, columns = 1..2))
    
        // Overlaps on top of HeroCard in cell (1, 1) aligned to TopStart!
        LiveBadge(
            modifier = Modifier.gridItem(
                row = 1,
                column = 1,
                alignment = Alignment.TopStart
            )
        )
    
        // Negative index: placed in last row (-1), spanning all 3 columns
        FooterBar(modifier = Modifier.gridItem(row = -1, column = 1, columnSpan = 3))
    }
  • আপনি যখন স্পষ্ট কন্ট্রোল চান, তখন আপনি Modifier.gridItem ব্যবহার করতে পারেন এবং আপনার কাছে এক-ভিত্তিক কোঅর্ডিনেট থাকে।

  • তাই, সারি ১, কলাম ১ হল উপরের বাঁদিকের সেল।

  • এটি নেগেটিভ ইন্ডেক্সিং সাপোর্ট করে, তাই -১ নম্বর সারি হল শেষ সারি এবং এইভাবে চলতে থাকে।

  • আপনি rowSpan ব্যবহার করে 2D এরিয়া জুড়ে কোনও আইটেম স্প্যান করতে পারেন এবং তারপরে ওভারল্যাপিংও আছে।

  • তাই, দুটি আইটেম ওভারল্যাপ করা সেলে রাখা হলে, Grid সেগুলিকে সেই সীমার মধ্যে একটির উপরে আরেকটি স্ট্যাক করে এবং কম্পোজিশন অর্ডার অনুযায়ী Z-অর্ডার নির্ধারণ করা হয়।

  • তাই, Box-এর মতোই পরের আইটেমগুলি অন্যদের উপরে আঁকা হবে।

  • এখন, আমরা গ্রিডের প্রাথমিক বিষয়গুলি জানি।

  • ভিডিও প্লেয়ারের উদাহরণ দেখা যাক।

  • ডিভাইসটি ফ্ল্যাট নাকি অর্ধেক খোলা আছে তার উপর নির্ভর করে, আমাদের ডিফল্ট লেআউট বা টেবিলটপ লেআউটের মধ্যে পরিবর্তন করতে হবে।

  • Grid-এর আগে, আমাদের দুটি আলাদা কম্পোজেবল, DefaultLayout এবং TabletopLayout তৈরি করতে হত এবং তারপরে when এক্সপ্রেশনে কোড ব্রাঞ্চ করতে হত।

    @Composable
    fun VideoPlayerLayout(
       player: @Composable () -> Unit,
       controller: @Composable () -> Unit,
       modifier: Modifier = Modifier
    ) {
       when(currentVideoPlayerLayout()) {
           VideoPlayerLayout.Default -> DefaultLayout(player, controller, modifier)
           VideoPlayerLayout.Tabletop -> TabletopLayout(player, controller, modifier)
       }
    }
  • আপনি হয়ত ভাবছেন, "এতে ভুল কী আছে?"

  • আপনি যদি সেই লেআউটগুলি দেখেন, তাহলে বুঝতে পারবেন যে সেগুলির কন্টেন্ট একই রকম।

  • দুটি ক্ষেত্রেই একই প্লেয়ার ও একই কন্ট্রোলার দেখানো হয়।

  • এই দুটির মধ্যে একমাত্র পার্থক্য হল, স্ক্রিনে এই দুটি এলিমেন্ট কীভাবে সাজানো হয়েছে।

  • এখনও পর্যন্ত, যেহেতু আমাদের কাছে লেআউট অ্যারেঞ্জমেন্টকে প্যারামিটার হিসেবে পাস করার কোনও উপায় ছিল না, তাই আমরা দুটি আলাদা কম্পোজেবল সংজ্ঞায়িত করতে বাধ্য হয়েছিলাম।

  • এখন, আমরা লেআউট কনফিগারেশন প্যারামিটারাইজ করতে পারি এবং একটি কম্পোজেবল ফাংশনের মধ্যে সবকিছু ম্যানেজ করতে পারি।

  • এবং আমরা দেখব কীভাবে।

  • তাই, Grid API-এর মাধ্যমে, আমরা নামযুক্ত এলাকাকে enum হিসেবে সংজ্ঞায়িত করতে পারি।

    enum class VideoPlayerArea { Player, Controller }
    
    Grid(
        config = { ... },
        modifier = modifier
    ) {
        Box(modifier = Modifier.gridItem(areaId = VideoPlayerArea.Player)) {
            player()
        }
        Box(modifier = Modifier.gridItem(areaId = VideoPlayerArea.Controller)) {
           controller()
       }
    }
  • তারপর, আমরা গ্রিডের মধ্যে রাখার জন্য চাইল্ড কম্পোজেবল, প্লেয়ার এবং কন্ট্রোলার নির্দিষ্ট করতে পারি এবং আমরা প্রতিটি চাইল্ডকে Modifier.gridItem কল করে একটি নামযুক্ত এলাকার সাথে যুক্ত করতে পারি এবং আমরা areaId পাস করি।

  • এবং এখানে আমরা Grid-কে বলছি প্লেয়ারকে VideoPlayerArea.Player নামের জায়গায় এবং কন্ট্রোলারকে VideoPlayerArea.Controller-এ রাখতে।

  • এবং এখানে আমরা VideoPlayerGridConfiguration-এ দুটি কনফিগারেশন ল্যাম্বডা সংজ্ঞায়িত করি।

    object VideoPlayerGridConfiguration {
       val Default: GridConfigurationScope.() -> Unit = {
           row(1f)
           column(1f)
           area(areaId = VideoPlayerArea.Player, row = 1, column = 1)
           area(areaId = VideoPlayerArea.Controller, row = 1, column = 1)
       }
    
       val Tabletop: GridConfigurationScope.() -> Unit = {
           row(0.5f)
           row(0.5f)
           column(1f)
           area(areaId = VideoPlayerArea.Player, row = 1, column = 1)
           area(areaId = VideoPlayerArea.Controller, row = 2, column = 1)
       }
    }
  • ডিফল্ট ভিডিও প্লেয়ার লেআউটের জন্য, আমরা একটি সারি ও একটি কলামের গ্রিড কনফিগার করি, তবে লক্ষ্য করুন যে, মাত্র একটি সেল থাকলেও, আমরা এখনও একই সেলে দুটি এরিয়ার নাম অ্যাসাইন করতে পারি।

  • এই ক্ষেত্রে, আমরা আগে যেমন দেখেছি, কন্ট্রোলারটি প্লেয়ারের উপরে ওভারলে করা হয়।

  • টেবিলটপের জন্য, আমরা শুধু দুটি সারি এবং একটি কলাম সংজ্ঞায়িত করি, প্লেয়ারটিকে উপরের অর্ধে এবং কন্ট্রোলারটিকে নীচের অর্ধে রাখি, মূলত, এইরকম।

  • আমরা একবার সেই কনফিগারেশন ল্যাম্বডা সংজ্ঞায়িত করার পরে, আমরা সরাসরি গ্রিডের কনফিগারেশনে MediaQuery ব্যবহার করতে পারি।

    @Composable
    fun VideoPlayerLayout(
       player: @Composable () -> Unit,
       controller: @Composable () -> Unit,
       modifier: Modifier = Modifier
    ) {
       Grid(
           config = { mediaQuery {
           if (isTabletopPosture) { VideoPlayerGridConfiguration.Tabletop }
     else { VideoPlayerGridConfiguration.Default }
    }
       }
           modifier = modifier
       ) {
           Box(modifier = Modifier.gridItem(areaId = VideoPlayerArea.Player)) {
               player()
           }
           Box(modifier = Modifier.gridItem(areaId = VideoPlayerArea.Controller)) {
               controller()
           }
       }
    }
  • তাই, এখন আমাদের কাছে UI-এর সাথে কম্পোজ করা যায় এমন একটি ভিডিও প্লেয়ার লেআউট আছে যা দুটি কনফিগারেশনের মধ্যে ডাইনামিকভাবে পরিবর্তন করে।

  • পরবর্তী যে প্রিমিটিভ নিয়ে আমি কথা বলতে চাই সেটি হল FlexBox।

    @Composable
    fun AdaptiveActionHeader(tags: List<String>) {
            FlexBox(
            config = {
                direction( if (isCompact)
    FlexDirection.Column else FlexDirection.Row )
                wrap(FlexWrap.Wrap)
                alignItems(FlexAlignItems.Center)
                gap(row = 8.dp, column = 12.dp)
            },
            modifier = Modifier.fillMaxWidth()
        ) {
            Text(
                text = "Home",
                modifier = Modifier.flex {
                    basis(FlexBasis.Auto)
                    grow(1f)
                    shrink(1f)
                }
            )
            tags.forEach { tag ->
                FilterChipItem(
                    label = tag,
                    modifier = Modifier.flex { shrink(0f) }
                )
            }
        }
    }
  • তাই, Grid যখন 2D সারি ও কলাম লেআউট ম্যানেজ করে, FlexBox, যা এখন স্টেবল, এক-মাত্রিক লেআউট ম্যানেজ করে যার এখনও ইন্টেলিজেন্ট স্পেস ডিস্ট্রিবিউশন, সঙ্কুচিত করা, মাল্টিলাইন র‍্যাপিং প্রয়োজন।

  • এবং এটি একটি সুবিধাজনক, প্রতিক্রিয়াশীল লেআউট যা আপনি দুটি পৃথক কম্পোজেবলের পরিবর্তে ব্যবহার করতে পারেন, যেমন FlowRow এবং FlowColumn, কারণ এটি একটি একক কন্টেনার যার দিকনির্দেশ চাইল্ড কম্পোজেবলগুলি পুনরায় তৈরি না করেই এর কনফিগারেশন ব্লকের ভিতরে গতিশীলভাবে পরিবর্তন হতে পারে।

  • এবং এটি সম্পূর্ণ CSS FlexBox অ্যালগরিদম প্রয়োগ করে, তাই আপনার কাছে র‍্যাপ, জাস্টিফাই-কন্টেন্ট, অ্যালাইন-আইটেম, গ্যাপ রয়েছে।

  • এবং প্রতিটি চাইল্ডে, আপনার কাছে Modifier.flex আছে যা আপনাকে বেসিস, রিলেটিভ গ্রো, শ্রিঙ্ক ফ্যাক্টর, alignSelf এবং আরও অনেক কিছু দেয়।

অ্যাপ-লেভেল লেআউট আর্কিটেকচার ও নেভিগেশন ৩

  • ফ্রান্সেসকো রোমানো

    ঠিক আছে, এখন স্ক্রিন-লেভেল সংজ্ঞা থেকে অ্যাপ-লেভেল লেআউটে জুম আউট করা যাক, কীভাবে আমরা আমাদের টপ-লেভেল নেভিগেশন এবং মাল্টি-পেন স্ক্রিন ব্যবস্থা পুরো অ্যাপ্লিকেশন জুড়ে স্ট্রাকচার করি, শুধু একটি স্ক্রিনে নয়।

  • MediaQuery আপনাকে Dp-তে উইন্ডোর সঠিক প্রস্থ এবং উচ্চতা প্রদান করে, অ্যাপ-লেভেল আর্কিটেকচারের জন্য, আপনি স্ট্যান্ডার্ডাইজড, ওপিনিয়নেটেড ভিউপোর্ট ব্রেকপয়েন্ট চান, যা উইন্ডো সাইজ ক্লাস।

  • মূলত, আপনি চান যে প্রতিটি স্ক্রিন যেন এইসব বাকেটের মধ্যে রেসপন্সিভ হয়, যেখানে আপনি আরও স্ট্রাকচারাল পরিবর্তন ও আপডেট করতে পারবেন এবং বিভিন্ন বাকেটের মধ্যে পার্থক্য করতে পারবেন।

  • যেমন, কম্প্যাক্ট স্ক্রিনে শুধু তালিকা বা বড় উইন্ডোতে তালিকা ও বিবরণ দুটিই দেখানো।

  • আপনি খুব পরিষ্কারভাবে সরাসরি UiMediaScope-এর উপরে উইন্ডো সাইজ ক্লাস তৈরি করতে পারেন, যা UiMediaScope-এ একটি এক্সটেনশন প্রপার্টি সংজ্ঞায়িত করছে।

    val UiMediaScope.windowSizeClass: WindowSizeClass
        @Composable get() = derivedMediaQuery {
            WindowSizeClass.BREAKPOINTS_V2.computeWindowSizeClass(
                widthDp = windowWidth.value,
                heightDp = windowHeight.value
            )
        }
    
    @Composable
    fun AppRoot() {
        val sizeClass = mediaQuery { windowSizeClass }
        val isExpandedWidth = sizeClass.isWidthAtLeastBreakpoint(
            WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND // 600dp
        )
        // Drive app-level navigation and scene strategies...
    }
  • একটি জিনিস লক্ষ্য করার মতো, এটি derivedMediaQuery ব্যবহার করছে, স্ট্যান্ডার্ড MediaQuery নয়।

  • কেন?

  • কারণ উইন্ডোর প্রস্থ ও উচ্চতা ঘন ঘন পরিবর্তিত হয়।

  • ব্যবহারকারীরা যখন আপনার অ্যাপের সাইজ পরিবর্তন করেন, যেমন ডেস্কটপে, তখন এই ভ্যালু প্রতিটি ফ্রেমে পরিবর্তন হতে পারে।

  • এবং আপনি সম্ভবত প্রতিটি ফ্রেমে আপনার অ্যাপ-লেভেল স্ক্যাফোল্ড রিকম্পোজ করতে চান না, তবে শুধুমাত্র যখন উইন্ডোটি আসলে একটি ব্রেকপয়েন্ট অতিক্রম করে।

  • আর একবার উইন্ডো সাইজ ক্লাস পেয়ে গেলে, আরও বেশি জায়গা থাকলে কীভাবে আমরা সিঙ্গেল-পেন ফ্লো থেকে মাল্টি-পেন লেআউটে আমাদের অ্যাপ ট্রানজিশন করব?

  • এখানেই Jetpack Navigation 3-এর কার্যকারিতা বোঝা যায়, কারণ Nav 3-এর আর্কিটেকচার Compose স্টেট ও সিনের উপর ভিত্তি করে তৈরি করা হয়েছে।

  • আর প্রথমে, আপনি ব্যাক স্ট্যাকের মালিক, যা NavKeys-এর একটি তালিকা, তাই এটি খুব নমনীয় এবং স্টেট হিসাবে সেভ করা সহজ।

  • স্ট্যাকের প্রতিটি কী NavEntry-তে সমাধান করে, যার মধ্যে কম্পোজেবল ও মেটাডেটা থাকে।

  • এবং NavDisplay কীভাবে এন্ট্রি আঁকা হয় তা হার্ড-কোড করে না; পরিবর্তে, এটি দৃশ্য কৌশলের একটি তালিকা মূল্যায়ন করে।

  • কৌশলের মধ্যে, আপনি UI ক্ষমতাগুলি পরীক্ষা করতে পারেন, উদাহরণস্বরূপ, উইন্ডো সাইজ ক্লাস।

    if (isExpandedWidth) {
        /* return a multi-pane scene */
    }
    else {
        /* return a single pane scene */
    }
  • আপনি ব্যাক স্ট্যাক পরীক্ষা করে দেখতে পারেন এবং তারপরে সিদ্ধান্ত নিতে পারেন যে একটি প্যানে একটি NavEntry রেন্ডার করবেন নাকি একসাথে একাধিক আইটেম রেন্ডার করবেন নাকি একটির উপরে আরেকটি রেন্ডার করবেন।

  • এটি খুব ফ্লেক্সিবল।

  • মাল্টি-পেন স্ট্র্যাটেজি ছাড়াও, উইন্ডো সাইজ ক্লাসও নির্ধারণ করে যে আপনি কোন নেভিগেশন কম্পোনেন্ট ব্যবহার করতে চান যাতে স্ক্রিন স্পেসের ব্যবহার এবং এরগোনমিক্স সর্বাধিক করা যায়।

  • যেমন, কম প্রস্থের স্ক্রিনে আপনি নিচের দিকে নেভিগেশন বার, মাঝারি প্রস্থের স্ক্রিনে NavRail এবং বেশি প্রস্থের স্ক্রিনে সম্পূর্ণ নেভিগেশন ড্রয়ার দেখতে চাইতে পারেন।

  • আপনি যখন অ্যাপ-লেভেল লেআউট সংক্রান্ত সিদ্ধান্তের জন্য Nav3 এবং স্ক্রিন-লেভেল সংজ্ঞার জন্য রেসপন্সিভ কম্পোনেন্ট একত্রিত করেন, তখন আপনি এমন অ্যাপ পান যা দেখতে এরকম এবং কার্যত প্রতিটি উইন্ডোর সাইজ এবং পরিবেশে উজ্জ্বল দেখায়।

  • বিশেষ করে Nav 3-এর জন্য, আপনি যদি ডিপ লিঙ্কের জন্য সহায়তা সহ লেটেস্ট আপডেট সম্পর্কে আরও জানতে চান, তাহলে Building Type-Safe Deep Links with Jetpack Navigation 3 টকটি দেখুন।

এজেন্টিক ডেভেলপমেন্ট ও অটোমেশন ডেমো

  • ফ্রান্সেসকো রোমানো

    ঠিক আছে, আমরা আজ অনেক নতুন API কভার করেছি: MediaQuery, Grid, FlexBox, Nav3.

  • এবং এটি Jetpack Compose কীভাবে আপনাকে অ্যাডাপ্টিভ অ্যাপ তৈরি করতে সাহায্য করে তা কভার করে।

  • কিন্তু এজেন্টিক ডেভেলপমেন্টের যুগে, Jetpack Compose ব্যবহার করার আরেকটি অত্যন্ত গুরুত্বপূর্ণ সুবিধা রয়েছে: এটি অনেক বেশি কার্যকর।

  • আপনি সম্ভবত মূল বক্তব্যের সময় এটি শুনেছেন, আমরা Compose-এ মাইগ্রেট করার পরে অ্যাপগুলি ৩০% কম টোকেন ব্যবহার করতে দেখেছি এবং আমরা Compose অ্যাপের জন্য আপনার এজেন্টিক ওয়ার্কফ্লো আরও বেশি অপ্টিমাইজ করার জন্য কাজ করছি।

  • আজ, আমি Compose Adaptive স্কিলের প্রধান আপডেটগুলি হাইলাইট করতে চাই।

    android skills list
    android skills add adaptive
  • As I said, in addition to migrating layouts to Grid, Flexbox, and MediaQuery, it now explicitly handle end-to-end state management across the app, and we test the skill against real-world state loss bugs: preserving navigation, scroll position, active media playback, half-typed text inputs, and so on.

  • এই স্কিলে এখন ফোল্ডেবল পশ্চার অপ্টিমাইজেশনও অন্তর্ভুক্ত, যেমন টেবিলটপ মোডে মিডিয়া এবং ক্যামেরা স্ক্রিন অটোমেটিক রিফ্যাক্টর করা, তবে আপনার অ্যাপ যখন Googlebook বা মাউস এবং কীবোর্ড সহ এক্সটার্নাল মনিটরে চলে, তখন লেআউটটি অর্ধেক কাজ করে।

  • অ্যাডাপ্টিভ স্কিল এখন আপনার UI অডিট করতে পারে যাতে কীবোর্ড শর্টকাট, কাস্টম পয়েন্টার কার্সর, হোভার স্টেট, টুলটিপ ইত্যাদি খুঁজে বের করে প্রয়োগ করা যায়।

  • তাহলে, এটি কীভাবে কাজ করে তা দেখা যাক।

  • আমি এটার একটা ডেমো রেকর্ড করেছি।

  • তাই আমাদের অ্যাপ আছে।

  • আমরা আমাদের পোষ্যদের দেখতে পারি।

  • আমরা তাদের সাথে মেসেজ করতে পারি।

  • আমরা তাদের একটি ভিডিও দেখতে পারি।

  • হ্যাঁ, আমরা সেরা পোষ্যদের দেখতে পাচ্ছি।

  • কিন্তু আপনি আগেই দেখেছেন যে এটি খুব একটা ভালো নয়।

  • এখন দেখা যাক, আমি এমন ছবি দেখতে চাই যাতে কিছু ড্রামা এবং বিড়াল আছে।

  • ঠিক আছে, আমি এগুলি আরও বড় করে দেখতে চাই।

  • ওহ, এটা তো হোম স্ক্রিনে ফিরে যাওয়ার জন্য।

  • ঠিক আছে, আমি আমার বন্ধুকে এই বিষয়ে জানাই।

  • হ্যাঁ, তাহলে, "বন্ধু", হুম।

  • অ্যাপটি ভাল নয়।

  • এছাড়াও, এটি খুব চওড়া।

  • আপনি দেখতে পাচ্ছেন যে এটি খুব চওড়া।

  • এটির সাইজ পরিবর্তন করে দেখা যাক।

  • কি?

  • হ্যাঁ, ঠিক আছে।

  • আর আমি যে টেক্সট টাইপ করেছি তা হারিয়ে গেছে।

  • ঠিক আছে, এবার Gemini-কে এটি ঠিক করতে বলুন।

  • তাই, অ্যাডাপ্টিভ স্কিলের জন্য ধন্যবাদ, প্রম্পটটি "অ্যাপটি অ্যাডাপ্টিভ করে তোলো"-এর মতো সহজ হতে পারে।

  • আপনি দেখতে পাচ্ছেন, আমি Gemini 3.0 Flash ব্যবহার করছি যা স্কিলের সাথে একসাথে এটি করতে পারে।

  • এটি স্কিলটি বেছে নিয়েছে এবং এখন পরিবর্তনগুলি রান করা শুরু করছে।

  • এটি কিছু প্রশ্ন জিজ্ঞাসা করবে, যেমন, "ওহ, MediaQuery হল পরীক্ষামূলক।

  • আপনি কি সত্যিই এটি ব্যবহার করতে চান?"

  • এবং চালিয়ে যান।

  • জিনিসপত্র কাটার জন্য আমাকে স্পিড বাড়াতে হয়েছিল, তবে আপনি ডেমো এরিয়াতে এলে, নিজেই এই স্কিল ব্যবহার করে দেখতে পারবেন।

  • আর এটি আমাদের রিলিজ করা স্কিলগুলির মধ্যে একটি মাত্র।

  • যেমন, আমাদের কাছে একটি নতুন দক্ষতাও আছে যা আপনাকে অসাধারণ স্টাইলাস অভিজ্ঞতা তৈরি করতে সাহায্য করবে।

  • তাই, এটি ব্যবহার করে দেখতে এই এলাকায় আসুন।

  • তাহলে, এটা হয়ে গেছে।

  • এটি ১১টি ফাইল পরিবর্তন করেছে।

  • এবং এটি কীভাবে কাজ করে তা দেখা যাক।

  • ঠিক আছে।

  • Grid আগে থেকেই তার কাজ করছে।

  • নেভিগেশনটি পাশে সরানো হয়েছে।

  • ফিল্টার ব্যবহার করে দেখা যাক।

  • আমার মনে হয়, এখন কাজ করবে।

  • হ্যাঁ, তারা করে।

  • ঠিক আছে, আমার বন্ধুকে মেসেজ করে বলি যে অ্যাপটি এখন দারুণ।

  • আরও বেশি স্পেস থাকলে কেমন দেখাবে তা দেখা যাক।

  • হ্যাঁ, এটি তালিকা-বিবরণ লেআউটে মাইগ্রেট করা হয়েছে।

  • এই রানটি করতে প্রায় ২০ মিনিট সময় লেগেছে এবং প্রায় ২০০,০০০ টোকেন ব্যবহার করা হয়েছে।

  • ঠিক আছে।

অ্যাডাপ্টিভ অ্যাপের ROI এবং মূল টেকঅ্যাওয়ে

  • ফ্রান্সেসকো রোমানো

    এখন, আমি সাধারণত ব্যবসার মেট্রিক এবং সাফল্যের কেস দিয়ে একটি আলোচনা শুরু করি, কিন্তু ADS-এর জন্য, আমি সরাসরি কোডে যেতে চেয়েছিলাম।

  • তবে, আমি ইঞ্জিনিয়ারিং রিটার্ন অফ ইনভেস্টমেন্ট শেয়ার করার জন্য কয়েক মিনিট সময় দিতে চাই।

  • বর্তমানে, আপনার Android অ্যাপ ৫৮০ মিলিয়নের বেশি অ্যাক্টিভ বড় স্ক্রিনের ডিভাইসে রান করতে পারে এবং আমরা জানি যে মাল্টি-ডিভাইস ও ফোল্ডেবল ডিভাইসের ব্যবহারকারীরা Android-এ সবচেয়ে বেশি এনগেজড থাকা ব্যবহারকারীদের মধ্যে অন্যতম।

  • আসুন, দুটি বাস্তব কেস স্টাডি দেখে নেওয়া যাক, যা থেকে বোঝা যাবে যে আপনি অ্যাডাপ্টিভ বিল্ড শিপ করলে কী হয়।

  • প্রথমটি হল Gratitude, এটি নিজের যত্ন নেওয়া ও দৈনিক জার্নালিংয়ের জন্য একটি জনপ্রিয় অ্যাপ।

  • অ্যাডাপ্টিভ স্ক্রিন ডিজাইন গ্রহণ করার মাধ্যমে, Gratitude ট্যাবলেট ও ফোল্ডেবল ডিভাইসকে উচ্চ-গ্রোথ অ্যাকুইজিশন চ্যানেল হিসেবে প্রতিষ্ঠা করেছে।

  • প্রথমত, তারা বড় স্ক্রিনে গ্রাহক অধিগ্রহণের গতি দ্বিগুণ করেছে।

  • ট্যাবলেট ও ফোল্ডেবল ডিভাইসে প্রতিদিন ইনস্টল করার হার ১০০% বৃদ্ধি পেয়েছে।

  • তারা মোট ইনস্টলের ৭% থেকে ১০% পর্যন্ত তাদের সামগ্রিক ব্যবহারকারীর বেসের বড়-স্ক্রিন শেয়ার প্রসারিত করেছে।

  • দ্বিতীয়টি হল BoxBoxClub, যা একটি ফর্মুলা ১ এবং মোটরস্পোর্ট কম্প্যানিয়ন অ্যাপ এবং ফোল্ডেবল ও বড় স্ক্রিনের জন্য তাদের বিল্ড আপডেট করার পরে, তাদের টেলিমেট্রি ব্যাপক লাভ দেখিয়েছে: সামগ্রিক অ্যাক্টিভ ব্যবহারকারীর সংখ্যা ৭.৬% বৃদ্ধি এবং এনগেজমেন্টের সময় ৫৪% বৃদ্ধি পেয়েছে।

  • ঠিক আছে।

  • আমরা আলোচনার শেষ পর্যায়ে পৌঁছে গেছি এবং আমি আপনাকে তিনটি গুরুত্বপূর্ণ টেকঅ্যাওয়ে দিতে চাই।

  • অ্যাডাপ্টিভ অ্যাপ তৈরি করলে আপনার অ্যাপ আরও বেশি ডিভাইস ও ব্যবহারকারীকে পরিষেবা দিতে পারবে এবং এই ক্রমবর্ধমান ইকোসিস্টেমের জন্য আপনার অ্যাপকে ভবিষ্যতের জন্য প্রস্তুত করবে।

  • Android ডেভেলপমেন্ট হল কম্পোজ ফার্স্ট।

  • Compose-এর জন্য সব নতুন টুল, লাইব্রেরি ও দক্ষতা তৈরি করা হয়েছে।

  • Compose অ্যাডাপ্টিভ লেআউট ও এজেন্টিক ডেভেলপমেন্টের জন্য দারুণ।

  • কোডবেস ছোট এবং বোঝা সহজ, Compose কোড নিয়ে কাজ করার সময় এজেন্ট কম টোকেন ব্যবহার করে।

  • তাই, এগিয়ে যান, সব নতুন টুল, দক্ষতা, API ব্যবহার করে দেখুন এবং মজা করুন।

  • ধন্যবাদ।

কোড

কোড স্নিপেট

"স্ক্রিন জুড়ে অ্যাপ স্কেলিংয়ের ভূমিকা" ভিডিওর 05:17-এ XML স্নিপেট
<LinearLayout android:orientation="vertical">
  <EditText
      android:id="@+id/text_msg"
      android:inputType="textCapCharacters"
      android:maxLength="12" />
</LinearLayout>
"স্ক্রিন জুড়ে অ্যাপ স্কেলিংয়ের ভূমিকা" ভিডিওর 05:17-এ XML স্নিপেট
<ConstraintLayout>
  <TextInputEditText
    android:id="@+id/text_input"
    android:inputType="text" />
</ConstraintLayout>
"Detecting postures with the mediaQuery API" ভিডিওর 11:31-এ Kotlin স্নিপেট
mediaQuery { ... }
derivedMediaQuery { ... }
"mediaQuery API-এর সাহায্যে পোস্টার শনাক্ত করা" ভিডিওর 11:57-এ Kotlin স্নিপেট
sealed interface UiMediaScope {
    @get:FrequentlyChangingValue val windowWidth: Dp
    @get:FrequentlyChangingValue val windowHeight: Dp

    val windowPosture: Posture // Flat, Tabletop, Book
    val pointerPrecision: PointerPrecision // Fine, Coarse, Blunt, None
    val keyboardKind: KeyboardKind // Physical, Virtual, None

    val hasCamera: Boolean
    val hasMicrophone: Boolean
    val viewingDistance: ViewingDistance // Near, Medium, Far
}
"mediaQuery API-এর সাহায্যে পোস্টার শনাক্ত করা" ভিডিওর 13:11-এ Kotlin স্নিপেট
@Composable
fun currentVideoPlayerLayout(): VideoPlayerLayout {
   val context = LocalContext.current
   val windowLayoutInfo by WindowInfoTracker.getOrCreate(context).windowLayoutInfo(context)
       .collectAsStateWithLifecycle(null)
   val isTabletopPosture = windowLayoutInfo?.displayFeatures?.any { displayFeature ->
       displayFeature is FoldingFeature &&
               displayFeature.state == FoldingFeature.State.HALF_OPENED &&
               displayFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
   } ?: false
   return if (isTabletopPosture) {
       VideoPlayerLayout.Tabletop
   } else {
       VideoPlayerLayout.Default
   }
}
"Detecting postures with the mediaQuery API" ভিডিওর 13:42-এ Kotlin স্নিপেট
@Composable
fun currentVideoPlayerLayout(): VideoPlayerLayout {
   return mediaQuery {
       when(windowPosture) {
           UiMediaScope.Posture.Tabletop -> VideoPlayerLayout.Tabletop
           else -> VideoPlayerLayout.Default
       }
   }
}
"Detecting postures with the mediaQuery API" ভিডিওর ১৪:০৭-এ Kotlin স্নিপেট
@Composable
fun AdaptiveChatControls(
    onStartVideoCall: () -> Unit,
    onSendMessage: () -> Unit,
) {
    // 1. Return a Dp value directly based on pointer precision
    val chipPadding = mediaQuery {
        if (pointerPrecision == PointerPrecision.Fine) 2.dp else 8.dp
    }

    // 2. Return a Boolean combining hardware capabilities
    val canVideoCall = mediaQuery { hasCamera && hasMicrophone }
    val hasPhysicalKeyboard = mediaQuery {
        keyboardKind == KeyboardKind.Physical
    }

    Row(modifier = Modifier.padding(chipPadding)) {
        if (canVideoCall) {
            IconButton(onClick = onStartVideoCall) {
                Icon(Icons.Default.Videocam, contentDescription = "Video call")
            }
        }
        Button(onClick = onSendMessage) {
            Text(if (hasPhysicalKeyboard) "Send (Ctrl+Enter)" else "Send")
        }
    }
}
"mediaQuery API-এর সাহায্যে পোস্টার শনাক্ত করা" ভিডিওর ১৪:৫৯-এ Kotlin স্নিপেট
@Preview(name = "Tabletop + Fine Pointer", widthDp = 840, heightDp = 700)
@Composable
fun TabletopPlayerPreview() {
    val simulatedMediaScope = object : UiMediaScope {
        override val windowWidth = 840.dp
        override val windowHeight = 700.dp
        override val windowPosture = UiMediaScope.Posture.Tabletop
        override val pointerPrecision = UiMediaScope.PointerPrecision.Fine
        override val keyboardKind = UiMediaScope.KeyboardKind.Physical
        override val hasCamera = true
        override val hasMicrophone = true
        override val viewingDistance = UiMediaScope.ViewingDistance.Medium
    }

    CompositionLocalProvider(LocalUiMediaScope provides simulatedMediaScope) {
        VideoPlayerScreen()
    }
}
"Grid ও FlexBox সহ রেসপন্সিভ স্ক্রিন লেআউট" ভিডিওর 15:40-এ Kotlin স্নিপেট
Grid(
    modifier = Modifier.fillMaxSize().padding(16.dp),
    config = {
        val maxWidthDp = constraints.maxWidth.toDp()
        val columnCount = if (maxWidthDp < 600.dp) 2 else 4

        repeat(columnCount) {
            column(1.fr)
        }
        row(GridTrackSize.Auto)
        row(1.fr)

        gap(row = 16.dp, column = 8.dp)
        flow = GridFlow.Row
    }
) {
    // GridScope children...
}
"Grid ও FlexBox সহ রেসপন্সিভ স্ক্রিন লেআউট" ভিডিওর ১৬:৫০-এ Kotlin স্নিপেট
Grid(
    config = {
        repeat(3) { column(1.fr) }
        repeat(3) { row(100.dp) }
        gap(8.dp)
    }
) {
    // Spans 2x2 area (rows 1..2, columns 1..2)
    HeroCard(modifier = Modifier.gridItem(rows = 1..2, columns = 1..2))

    // Overlaps on top of HeroCard in cell (1, 1) aligned to TopStart!
    LiveBadge(
        modifier = Modifier.gridItem(
            row = 1,
            column = 1,
            alignment = Alignment.TopStart
        )
    )

    // Negative index: placed in last row (-1), spanning all 3 columns
    FooterBar(modifier = Modifier.gridItem(row = -1, column = 1, columnSpan = 3))
}
"Grid ও FlexBox সহ রেসপন্সিভ স্ক্রিন লেআউট" ভিডিওর ১৭:৫৯-এ Kotlin স্নিপেট
@Composable
fun VideoPlayerLayout(
   player: @Composable () -> Unit,
   controller: @Composable () -> Unit,
   modifier: Modifier = Modifier
) {
   when(currentVideoPlayerLayout()) {
       VideoPlayerLayout.Default -> DefaultLayout(player, controller, modifier)
       VideoPlayerLayout.Tabletop -> TabletopLayout(player, controller, modifier)
   }
}
"Grid ও FlexBox-এর মাধ্যমে রেসপন্সিভ স্ক্রিন লেআউট" ভিডিওর ১৮:৫১-এ Kotlin স্নিপেট
enum class VideoPlayerArea { Player, Controller }

Grid(
    config = { ... },
    modifier = modifier
) {
    Box(modifier = Modifier.gridItem(areaId = VideoPlayerArea.Player)) {
        player()
    }
    Box(modifier = Modifier.gridItem(areaId = VideoPlayerArea.Controller)) {
       controller()
   }
}
"Grid ও FlexBox সহ রেসপন্সিভ স্ক্রিন লেআউট" ভিডিওর ১৯:২০-এ Kotlin স্নিপেট
object VideoPlayerGridConfiguration {
   val Default: GridConfigurationScope.() -> Unit = {
       row(1f)
       column(1f)
       area(areaId = VideoPlayerArea.Player, row = 1, column = 1)
       area(areaId = VideoPlayerArea.Controller, row = 1, column = 1)
   }

   val Tabletop: GridConfigurationScope.() -> Unit = {
       row(0.5f)
       row(0.5f)
       column(1f)
       area(areaId = VideoPlayerArea.Player, row = 1, column = 1)
       area(areaId = VideoPlayerArea.Controller, row = 2, column = 1)
   }
}
"Grid ও FlexBox সহ রেসপন্সিভ স্ক্রিন লেআউট" ভিডিওতে 20:00-এ Kotlin স্নিপেট
@Composable
fun VideoPlayerLayout(
   player: @Composable () -> Unit,
   controller: @Composable () -> Unit,
   modifier: Modifier = Modifier
) {
   Grid(
       config = { mediaQuery {
       if (isTabletopPosture) { VideoPlayerGridConfiguration.Tabletop }
 else { VideoPlayerGridConfiguration.Default }
}
   }
       modifier = modifier
   ) {
       Box(modifier = Modifier.gridItem(areaId = VideoPlayerArea.Player)) {
           player()
       }
       Box(modifier = Modifier.gridItem(areaId = VideoPlayerArea.Controller)) {
           controller()
       }
   }
}
"Grid ও FlexBox সহ রেসপন্সিভ স্ক্রিন লেআউট" ভিডিওর ২০:১৬-এ Kotlin স্নিপেট
@Composable
fun AdaptiveActionHeader(tags: List<String>) {
        FlexBox(
        config = {
            direction( if (isCompact)
FlexDirection.Column else FlexDirection.Row )
            wrap(FlexWrap.Wrap)
            alignItems(FlexAlignItems.Center)
            gap(row = 8.dp, column = 12.dp)
        },
        modifier = Modifier.fillMaxWidth()
    ) {
        Text(
            text = "Home",
            modifier = Modifier.flex {
                basis(FlexBasis.Auto)
                grow(1f)
                shrink(1f)
            }
        )
        tags.forEach { tag ->
            FilterChipItem(
                label = tag,
                modifier = Modifier.flex { shrink(0f) }
            )
        }
    }
}
"অ্যাপ-লেভেল লেআউট আর্কিটেকচার এবং নেভিগেশন ৩" ভিডিওর ২২:০৯-এ Kotlin স্নিপেট
val UiMediaScope.windowSizeClass: WindowSizeClass
    @Composable get() = derivedMediaQuery {
        WindowSizeClass.BREAKPOINTS_V2.computeWindowSizeClass(
            widthDp = windowWidth.value,
            heightDp = windowHeight.value
        )
    }

@Composable
fun AppRoot() {
    val sizeClass = mediaQuery { windowSizeClass }
    val isExpandedWidth = sizeClass.isWidthAtLeastBreakpoint(
        WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND // 600dp
    )
    // Drive app-level navigation and scene strategies...
}
"অ্যাপ-লেভেল লেআউট আর্কিটেকচার এবং নেভিগেশন ৩" ভিডিওর ২৩:৩৩-এ Kotlin স্নিপেট
if (isExpandedWidth) {
    /* return a multi-pane scene */
}
else {
    /* return a single pane scene */
}
"এজেন্টিক ডেভেলপমেন্ট এবং অটোমেশন ডেমো" ভিডিওর ২৫:১৮-এ শেল স্নিপেট
android skills list
android skills add adaptive