Android ईकोसिस्टम बहुत बड़ा है. इसमें उपयोगकर्ता, फ़ोन, टैबलेट, फ़ोल्ड किए जा सकने वाले डिवाइसों, और डेस्कटॉप एनवायरमेंट के बीच आसानी से स्विच कर सकते हैं. अब हम Compose First का इस्तेमाल कर रहे हैं. इस सेशन में, हम यह जानेंगे कि रीसाइज़ की जा सकने वाली विंडो और अलग-अलग आसपेक्ट रेशियो (लंबाई-चौड़ाई का अनुपात) के लिए, अपने ऐप्लिकेशन को कैसे डिज़ाइन करें. ऐसा इसलिए, क्योंकि लेआउट के पारंपरिक तरीके काम नहीं करते. इससे यूज़र इंटरफ़ेस (यूआई) स्ट्रेच हो जाता है और स्टेट खो जाती है.
जानें कि Jetpack Compose, Navigation 3, और WindowManager की नई सुविधाएं, डाइनैमिक लेआउट बनाने में कैसे मदद कर सकती हैं. ये लेआउट, छोटी स्क्रीन से लेकर स्प्लिट स्क्रीन की सुविधा वाले डिसप्ले तक, हर तरह की स्क्रीन पर अपने-आप अडजस्ट हो जाते हैं. Gemini और हमारी स्किल का इस्तेमाल करके, डेवलपमेंट के अपने वर्कफ़्लो को बेहतर बनाने की रणनीतियां जानें. जैसे, अडैप्टिव स्किल, ताकि बड़ी स्क्रीन के लिए लेगसी कोड बेस को अपने-आप रीफ़ैक्टर किया जा सके.
MediaQuery एपीआई, यूज़र इंटरफ़ेस (यूआई) की चालू सुविधाओं को डिक्लेरेटिव तरीके से ट्रैक करता है. जैसे, विंडो का साइज़ और फ़ोल्ड करने के पोस्चर. यह Compose स्टेट में, कई लेगसी प्लैटफ़ॉर्म मैनेजर को एक साथ जोड़ने की जगह लेता है.Grid और FlexBox प्रिमिटिव, अडैप्टिव लेआउट के बिल्डिंग ब्लॉक उपलब्ध कराते हैं. Grid, एक ही कंपोज़ेबल में पैरामीटर के हिसाब से कई डाइमेंशन वाले अरेंजमेंट की सुविधा देता है. वहीं, FlexBox, एक डाइमेंशन वाले स्पेस डिस्ट्रिब्यूशन और रैपिंग की सुविधा देता है.ट्रांसक्रिप्ट
फ़्रांसेस्को रोमानो
नमस्ते.
आज हम Jetpack Compose की मदद से, आपके ऐप्लिकेशन को हर स्क्रीन के हिसाब से अडैप्ट करने के बारे में तकनीकी तौर पर गहराई से जानेंगे.
हर स्क्रीन के लिए, अपनी स्ट्रीम को ज़रूरत के मुताबिक क्यों बनाएं?
ऐसा इसलिए, क्योंकि Android ईकोसिस्टम अब कई अलग-अलग साइज़, डाइमेंशन या कॉन्फ़िगरेशन वाले डिवाइसों पर उपलब्ध है.
आपका APK, फ़ोन, फ़ोल्ड किए जा सकने वाले डिवाइसों, टैबलेट, Chromebook, Googlebook, टीवी, सूचना और मनोरंजन की सुविधा देने वाले डिवाइसों, एक्सआर डिवाइसों वगैरह पर चल सकता है.
सबसे पहले, अपने उपयोगकर्ताओं से वहीं जुड़ें जहां वे मौजूद हैं.
दूसरा, Android प्लैटफ़ॉर्म पर अडैप्टिव ऐप्लिकेशन का इस्तेमाल बढ़ रहा है. यह कोई अस्थायी बदलाव नहीं है.
ऐसा नहीं है कि हम सिर्फ़ फ़ोन वाली दुनिया में वापस जा रहे हैं. यह सुविधा अब हमेशा के लिए उपलब्ध रहेगी.
मेरे पास यहां एक छोटी सी टाइमलाइन है.
साल 2022 में, हमने इस दिशा में काम करना शुरू किया.
हमने Android 12L और Jetpack WindowManager की पहली रिलीज़ लॉन्च की. साथ ही, आपको विंडो साइज़ क्लास जैसे एपीआई उपलब्ध कराए. इसके अलावा, हमने अलग-अलग डिवाइसों के हिसाब से ढल जाने वाले ऐप्लिकेशन की क्वालिटी के लिए, दिशा-निर्देशों का पहला वर्शन पब्लिश किया.
इसके बाद, हमने Google Play को भी अपडेट किया, ताकि रेटिंग को डिवाइस के टाइप के हिसाब से बांटा जा सके. साथ ही, हमने रैंकिंग से जुड़े फ़ैसलों में, अलग-अलग डिवाइसों के हिसाब से ऐप्लिकेशन की क्वालिटी के दिशा-निर्देशों को शामिल करना शुरू किया.
Android 15 में, हमने एओएसपी में डेस्कटॉप विंडोइंग की सुविधा भी जोड़ी है. साथ ही, Compose Material 3 अडैप्टिव को स्टेबल वर्शन में उपलब्ध कराया है.
इसके बाद, Android 16, SDK 36 से, सिस्टम बड़ी स्क्रीन पर ओरिएंटेशन लॉक, आसपेक्ट रेशियो (लंबाई-चौड़ाई का अनुपात) लॉक, और साइज़ बदलने वाले फ़्लैग को अनदेखा कर देता है.
कनेक्टेड डिसप्ले की सुविधा के साथ, अब आपका APK किसी भी विंडो साइज़ में चल सकता है.
SDK 36 में, आपको एक ऐसा अस्थायी मेनिफ़ेस्ट फ़्लैग मिला था जिससे ओरिएंटेशन और आसपेक्ट रेशियो (लंबाई-चौड़ाई का अनुपात) को बदलने की सुविधा से ऑप्ट आउट किया जा सकता था.
Android 17 में, बाहर होने का विकल्प हटा दिया गया है.
इसलिए, अडैप्टिव लेआउट बनाना, प्लैटफ़ॉर्म की एक बुनियादी ज़रूरत है.
पिछले दशक में, हमने पूरे नेटवर्क की टीमों के साथ मिलकर काम किया. इस दौरान, हमें पता चला कि अडैप्टिव ऐप्लिकेशन बनाना मुश्किल क्यों हो सकता है.
आपको कुछ स्टेट लॉस, फ़्रैगमेंटेड विंडो एपीआई, लेआउट स्विच करने के बारे में साफ़ तौर पर सिग्नल न मिलने जैसी समस्याएं आ सकती हैं. साथ ही, कोड बेस में कुछ पुरानी धारणाएं भी शामिल हो सकती हैं.
इसलिए, हमने पिछले दशक में डेवलपर को आने वाली समस्याओं से जो कुछ भी सीखा उसे सीधे दो जगहों पर लागू किया: पहला, यूज़र इंटरफ़ेस (यूआई) टूलकिट में.
आज हम Jetpack Compose की कुछ नई और अहम सुविधाओं के बारे में जानेंगे: मीडिया क्वेरी, ग्रिड, FlexBox, और Navigation 3.
इसके अलावा, Android Studio में Gemini के लिए Android की आधिकारिक स्किल और Android का सीएलआई की मदद से, आपके साथ कोड लिखने वाले एजेंटिक टूल में भी Gemini 3 का इस्तेमाल किया जा सकता है.
उदाहरण के लिए, हम Compose की अडैप्टिव स्किल देखेंगे. इसे इस साल की शुरुआत में Google I/O में रिलीज़ किया गया था.
यह पहले से ही फ़ोन के लेआउट को बड़ी विंडो के साइज़ में माइग्रेट कर सकता था.
अब यह आपके ऐप्लिकेशन की स्टेट लॉस की समस्या को भी ठीक कर सकता है. साथ ही, फ़ोल्ड किए जा सकने वाले डिवाइसों के लिए यूनीक अनुभव लागू कर सकता है, जैसे कि टेबलटॉप. इसके अलावा, कीबोर्ड शॉर्टकट, कस्टम कर्सर, टूलटिप वगैरह को ढूंढकर और लागू करके, इनपुट की सुविधा को बेहतर बना सकता है.
चलिए, Jetpack Compose से शुरुआत करते हैं. यह इन सभी का आधार है.
Jetpack Compose को 2021 में स्टेबल वर्शन के तौर पर लॉन्च किया गया था. इसलिए, यह इसका पांचवां जन्मदिन है.
इन पांच सालों में, हमने प्रोडक्शन में मौजूद हज़ारों ऐप्लिकेशन में देखा है कि Compose की मदद से, अडैप्टिव लेआउट बनाना कितना आसान और भरोसेमंद है.
Android डेवलपमेंट अब आधिकारिक तौर पर Compose-फ़र्स्ट है.
इसका मतलब है कि सभी नए मॉडर्न अडैप्टिव लेआउट, स्टाइल, और नेविगेशन कॉम्पोनेंट, Compose के लिए बनाए गए हैं.
सिस्टम आर्किटेक्चर के नज़रिए से, लेगसी व्यू सिस्टम के मुकाबले अडैप्टिव ऐप्लिकेशन के लिए Compose का सबसे बड़ा फ़ायदा यह है कि इसमें स्टेट और यूज़र इंटरफ़ेस (यूआई) के बीच इंटरैक्शन होता है.
इसे समझाने के लिए, मैं व्यू सिस्टम के साथ इसकी तुलना करना चाहता हूं.
इसलिए, सोचें कि व्यू सिस्टम कैसे काम करता था.
आपका यूज़र इंटरफ़ेस (यूआई), बदलाव किए जा सकने वाले ऑब्जेक्ट का एक ट्री था. इसे आपने स्टैटिक एक्सएमएल फ़ाइलों से एक बार इनफ़्लेट किया था.
आपके पास अलग-अलग रिसॉर्स क्वालिफ़ायर थे. चलिए, यहां मुख्य समस्या से शुरुआत करते हैं: अलग-अलग रिसॉर्स क्वालिफ़ायर में डुप्लीकेट होना.
एक्सएमएल में अडैप्टिव यूज़र इंटरफ़ेस (यूआई) बनाते समय, लेआउट फ़ाइल में कोई कंडिशनल लॉजिक नहीं होता.
इसके बजाय, आपको रिसॉर्स फ़ोल्डर में पूरी हैरारकी को डुप्लीकेट करना होगा. जैसे, लेआउट, लेआउट की सबसे छोटी विंडो 600 डीपी, लेआउट लैंड वगैरह.
ठीक है, दाईं ओर मौजूद 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>यह वही लॉजिकल कॉम्पोनेंट है जो टेक्स्ट इनपुट को हैंडल करता है. साथ ही, यह अलग-अलग एक्सएमएल फ़ाइलों में मौजूद होता है.
अगर आपने फ़ोन लेआउट में inputType या maxLength जैसे किसी एट्रिब्यूट को अपडेट किया है, तो कंपाइलर आपको टैबलेट लेआउट में भी उसे अपडेट करने के लिए मजबूर नहीं करता.
साथ ही, कॉम्पोनेंट अलग-अलग फ़ाइलों में बंटे होते हैं. इसलिए, समय के साथ स्ट्रक्चर में बदलाव होना तय है.
यह डुप्लीकेट डेटा, एक अन्य समस्या से भी जुड़ा है. वह समस्या है, कॉन्फ़िगरेशन में बदलाव के दौरान स्टेट लॉस होना.
याद रखें कि व्यू सिस्टम में, यूज़र इंटरफ़ेस (यूआई) कॉम्पोनेंट स्टेटफ़ुल होते हैं.
EditText के पास, बदलाव किए जा सकने वाले इंटरनल बफ़र, कस्टम कर्सर की पोज़िशन, फ़ोकस की स्थिति, और कुछ अन्य स्थितियों का मालिकाना हक होता है.
इसलिए, जब कोई उपयोगकर्ता डिवाइस को अनफ़ोल्ड करता है या विंडो का साइज़ किसी तय थ्रेशोल्ड से ज़्यादा बदलता है, तो Android पूरी व्यू हैरारकी को खत्म कर देता है और नई एक्सएमएल फ़ाइलों को शुरू से इनफ़्लेट करता है.
उपयोगकर्ता के इनपुट को सेव करने के लिए, फ़्रेमवर्क onSaveInstanceState पर निर्भर करता है. यह उस व्यू के आईडी से लिंक की गई स्थिति को पैक करेगा.
हालांकि, इससे तीन संभावित गड़बड़ियां हो सकती हैं.
सबसे पहले, अगर किसी ने दूसरे एक्सएमएल में आईडी का नाम गलती से text_msg के बजाय text_input रख दिया, तो स्टेट रीस्टोरेशन की सुविधा, उपयोगकर्ता के इनपुट टेक्स्ट को बिना किसी चेतावनी के हटा देती है.
दूसरा, onRestoreInstanceState, onViewCreated के बाद चलता है. यह अक्सर ViewModel ऑब्ज़र्वर से टकरा सकता है. इससे कॉलबैक लूप बन सकते हैं.
तीसरी बात, अगर क्वालिफ़ायर के बीच विजेट टाइप को स्वैप किया जाता है, तो मान लें कि आपको कॉम्पैक्ट स्क्रीन पर स्पिनर चाहिए और बड़ी स्क्रीन पर RadioGroup चाहिए. अगर वे एक ही आईडी शेयर करते हैं, तो ClassCastException की वजह से रनटाइम के दौरान क्रैश हो जाता है.
इसके बजाय, Jetpack Compose में आपका यूज़र इंटरफ़ेस (यूआई) ऐसा ऑब्जेक्ट नहीं है जिसे सिर्फ़ एक बार बनाया जाता है.
आपका यूआई, स्टेट का एक प्योर फ़ंक्शन है.
इससे कॉन्फ़िगरेशन में होने वाले बदलाव का मतलब पूरी तरह से बदल जाता है.
Compose में कॉन्फ़िगरेशन में बदलाव क्या है?
मेरे पास एक हिंट है.
फ़्रांसेस्को रोमानो
हाँ।
Compose में, कॉन्फ़िगरेशन में बदलाव करना, फिर से कंपोज़ करने जैसा ही है. साथ ही, फिर से कंपोज़ करने की प्रोसेस हमेशा चलती रहती है.
जब विंडो बड़ी हो जाती है, तो हो सकता है कि उपयोगकर्ता ने डिवाइस को अनफ़ोल्ड किया हो. ऐसे में, आपके कंपोज़ेबल नए साइज़ को पढ़ते हैं और जिनका आउटपुट इस पर निर्भर करता है वे फिर से चलते हैं.
बस, यही पूरा इवेंट है.
अब देखते हैं कि Compose के नए टूल और कॉम्पोनेंट का इस्तेमाल करके, अडैप्टिव ऐप्लिकेशन बनाने का क्या मतलब है.
इसलिए, एक्सएमएल की दुनिया में आपको इस तरह के सवाल पूछने पड़ते थे, "क्या मैं फ़ोन, टैबलेट या डेस्कटॉप पर काम कर रहा हूं?" इसके बाद, डिवाइस की कैटगरी के आधार पर यूज़र इंटरफ़ेस (यूआई) को ब्रांच करना पड़ता था.
हालांकि, ऐसा न करने की एक बुनियादी वजह है.
एक ही फ़िज़िकल डिवाइस पर चलने के बावजूद, ऐप्लिकेशन का कॉन्टेक्स्ट और यूज़र इंटरफ़ेस (यूआई) की सुविधाएं, रनटाइम के दौरान डाइनैमिक तौर पर बदल सकती हैं.
इसलिए, मैं यह मैसेज देना चाहता हूं कि यूज़र इंटरफ़ेस (यूआई) से जुड़े फ़ैसले, मौजूदा ऐक्टिव क्षमताओं के हिसाब से लिए जाने चाहिए: मौजूदा विंडो का साइज़, पॉस्चर, और हार्डवेयर की कुछ विशेषताएं.
चलिए, इसका एक बेहतरीन उदाहरण देखते हैं. यह उदाहरण, बाहरी मॉनिटर पर डेस्कटॉप विंडोइंग की सुविधा का है.
अगर Pixel फ़ोन को डेस्क पर मौजूद किसी बाहरी मॉनिटर से कनेक्ट किया जाता है, तो 6 इंच की पोर्ट्रेट स्क्रीन पर चल रहा ऐप्लिकेशन प्रोसेस, अब 32 इंच के वाइड मॉनिटर पर चलेगा. इसमें फ़िज़िकल कीबोर्ड और माउस भी अटैच किया जा सकता है.
इसलिए, यह जांच करने से कि ऐप्लिकेशन किस तरह के डिवाइस पर चल रहा है, इस बात की गारंटी नहीं मिलती कि वह असल में किस स्क्रीन पर दिख रहा है या इनपुट के तरीके क्या हैं.
इसके अलावा, Android पर मल्टी-विंडो की सुविधा काम करती है. इसलिए, विंडो का साइज़ बदला जा सकता है.
भले ही, Googlebook की स्क्रीन फ़ोन से बड़ी हो, लेकिन हो सकता है कि आपका ऐप्लिकेशन, अन्य ऐप्लिकेशन के साथ 400 डीपी चौड़ी विंडो पर चल रहा हो.
इसलिए, स्टैटिक फ़िज़िकल डिसप्ले मेट्रिक के लिए क्वेरी करने का भी कोई मतलब नहीं है.
इसलिए, Compose के लिए ऐप्लिकेशन बनाने से पहले, हमें यह समझना होगा कि एनवायरमेंट के बारे में क्वेरी कैसे की जाती है.
सबसे पहले, मैं यूज़र इंटरफ़ेस की सुविधाओं के कॉन्सेप्ट को औपचारिक तौर पर समझाना चाहता हूं.
यूज़र इंटरफ़ेस (यूआई) की सुविधाओं में, विंडो की चौड़ाई और ऊंचाई या फ़ोल्ड किए जा सकने वाले डिवाइस का पॉस्चर ही शामिल नहीं होता. इसमें इनपुट और आउटपुट के वे मोड भी शामिल होते हैं जो फ़िलहाल चालू हैं.
हमने देखा है कि डेस्कटॉप एनवायरमेंट में, उपयोगकर्ता हमेशा टच स्क्रीन का इस्तेमाल नहीं करते.
वे कीबोर्ड और माउस का इस्तेमाल कर सकते हैं.
ऑटोमोटिव सिस्टम में, कुछ कारों में रोटरी नॉब या अलग-अलग इनपुट होते हैं. साथ ही, सिग्नल इनपुट करने का तरीका भी अलग होता है.
साथ ही, स्क्रीन के साइज़ और इनपुट के अलावा, उपयोगकर्ता डिसप्ले से कितनी दूर है, जैसे कि देखने की दूरी या हार्डवेयर कैमरा या माइक्रोफ़ोन फ़िलहाल कनेक्ट है या नहीं.
यूज़र इंटरफ़ेस (यूआई) की ये सभी सुविधाएं हैं.
फ़्रांसेस्को रोमानो
अब, अगर आपको सभी सिग्नल वापस पाने हैं, तो आपको अलग-अलग सोर्स से पांच या छह अलग-अलग एपीआई को एक साथ जोड़ना होगा.
आपको कॉन्फ़िगरेशन ऑब्जेक्ट, WindowMetricsCalculator, WindowInfoTracker, InputManager, और PackageManager की ज़रूरत होती थी. इसके बाद, आपको मैन्युअल तरीके से हर एक को एक स्टेट, Compose स्टेट में बदलना पड़ता था.
क्या आपको ऐसा करना पसंद आया?
नहीं, मैं खु़द इसे नहीं पहचान पाई।
इस समस्या को हल करने के लिए, हमने androidx.compose.ui में MediaQuery API को लॉन्च किया.
mediaQuery { ... }
derivedMediaQuery { ... }यह सीएसएस मीडिया क्वेरी से प्रेरित है.
साथ ही, मीडिया का मतलब ऑडियो या वीडियो प्लेबैक नहीं है.
यह उस मीडियम या एनवायरमेंट को दिखाता है जिस पर आपका यूज़र इंटरफ़ेस (यूआई) रेंडर किया जा रहा है.
MediaQuery की मदद से, यूज़र इंटरफ़ेस (यूआई) की सुविधाओं के बारे में क्वेरी करने के लिए, Compose-नेटिव तरीका इस्तेमाल किया जा सकता है.
आइए, 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 से आपको प्राइमरी पॉइंटिंग डिवाइस की सटीक जानकारी मिलती है: माउस, स्टाइलस या ट्रैकपैड के लिए Fine; टच के लिए Coarse; जॉयस्टिक, कंट्रोलर या None के लिए Blunt.
keyboardKind की वैल्यू, physical, virtual या none हो सकती है.
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 को इकट्ठा करना पड़ता था. यह एक फ़्लो है, जो लाइफ़साइकल के बारे में जानकारी देने वाला Compose स्टेट है. इसके बाद, displayFeatures को दोहराना पड़ता था, ताकि यह पता चल सके कि क्या कोई ऐसा FoldingFeature है जो आधा खुला है और हॉरिज़ॉन्टल है.
इसका इस्तेमाल सिर्फ़ पॉस्चर का पता लगाने के लिए किया जाता है.
यह बहुत ज़्यादा बॉयलरप्लेट है.
नए MediaQuery API की मदद से, यह सब कोड की कुछ लाइनों में सिमट जाता है.
@Composable
fun currentVideoPlayerLayout(): VideoPlayerLayout {
return mediaQuery {
when(windowPosture) {
UiMediaScope.Posture.Tabletop -> VideoPlayerLayout.Tabletop
else -> VideoPlayerLayout.Default
}
}
}mediaQuery फ़ंक्शन, एक लैम्ब्डा पैरामीटर लेता है. साथ ही, उस लैम्ब्डा के रिसीवर स्कोप में, यह उन प्रॉपर्टी के सेट को दिखाता है जिन्हें हमने अभी देखा है.
इस उदाहरण में, when एक्सप्रेशन में windowPosture को देखकर, हम तुरंत सही कंपोज़ेबल दिखा सकते हैं.
एपीआई इस्तेमाल करने के दो और उदाहरण यहां दिए गए हैं.
@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")
}
}
}पहला है, जानकारी का लेवल.
इसलिए, जब कोई उपयोगकर्ता लैपटॉप या माउस वाले डेस्कटॉप मॉनिटर का इस्तेमाल करता है और पॉइंटर की सटीक जानकारी फ़ाइन होती है, तो हम अपने यूज़र इंटरफ़ेस (यूआई) कॉम्पोनेंट पर पैडिंग को टाइट कर सकते हैं. इस उदाहरण में, ज़्यादा कॉन्टेंट को आराम से दिखाने के लिए, 8 डीपी से 2 डीपी तक की पैडिंग की जा सकती है. ऐसा इसलिए, क्योंकि उपयोगकर्ता अपनी उंगलियों का इस्तेमाल नहीं कर रहे हैं, इसलिए टच टारगेट छोटे हो सकते हैं.
साथ ही, क्षमता के हिसाब से कार्रवाइयां की जा सकती हैं.
मान लें कि हमारे पास चैट इनपुट बार है. हम 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 दिया जा सकता है. साथ ही, Compose यूज़र इंटरफ़ेस (यूआई) टेस्ट चलाने के लिए, आपको फ़ोल्ड किए जा सकने वाले डिवाइसों को चालू करने और 90° पर रखने या माउस अटैच करने की ज़रूरत नहीं है.
फ़्रांसेस्को रोमानो
ठीक है, अब हमें यूज़र इंटरफ़ेस (यूआई) की सुविधाओं के बारे में क्वेरी करने का तरीका पता है.
अब देखते हैं कि असल लेआउट कैसे बनाया जाता है.
इसके लिए, मैं आपको ग्रिड के बारे में बताना चाहता हूं. यह 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...
}ग्रिड की मदद से, दो डाइमेंशन वाला लेआउट लागू किया जा सकता है. इसके लिए, आइटम को कॉलम और लाइनों में दिखाया जाता है. ये आइटम, उपलब्ध कंटेनर के साइज़ के हिसाब से अडजस्ट हो जाते हैं.
आइए, config ब्लॉक में किए जा सकने वाले कुछ काम देखते हैं.
कॉलम या लाइन को कॉल करके, नए एलिमेंट या ट्रैक जोड़े जा सकते हैं. साथ ही, Kotlin के स्टैंडर्ड लूप इस्तेमाल किए जा सकते हैं, जैसे कि repeat.
ट्रैक के बीच गटर सेट किए जा सकते हैं.
आपके पास flow-- 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 का इस्तेमाल किया जा सकता है. साथ ही, आपके पास एक-आधारित निर्देशांक होते हैं.
इसलिए, पंक्ति 1, कॉलम 1 सबसे ऊपर बाईं ओर मौजूद सेल है.
यह नेगेटिव इंडेक्सिंग के साथ काम करता है. इसलिए, पंक्ति -1 आखिरी पंक्ति होती है और इसी तरह यह क्रम चलता है.
rowSpan का इस्तेमाल करके, किसी आइटम को 2D एरिया में फैलाया जा सकता है. इसके बाद, ओवरलैप भी किया जा सकता है.
इसलिए, अगर दो आइटम ओवरलैप होने वाली सेल में रखे जाते हैं, तो ग्रिड उन्हें उन सीमाओं के अंदर एक-दूसरे के ऊपर स्टैक कर देता है. साथ ही, Z-ऑर्डर, कंपोज़िशन के क्रम से तय होता है.
इसलिए, बाद में जोड़े गए आइटम, पहले से मौजूद आइटम के ऊपर दिखेंगे. ठीक वैसे ही जैसे Box में दिखते हैं.
अब हमें ग्रिड की बुनियादी बातें पता हैं.
आइए, वीडियो प्लेयर का उदाहरण देखते हैं.
डिवाइस फ़्लैट है या आधा खुला है, इसके आधार पर हमें डिफ़ॉल्ट लेआउट या टेबलटॉप लेआउट के बीच स्विच करना होगा.
ग्रिड से पहले, हमें दो अलग-अलग कंपोज़ेबल, 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 को पास कर सकते हैं.
यहां हम ग्रिड को बता रहे हैं कि प्लेयर को 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()
}
}
}इसलिए, अब हमारे पास यूज़र इंटरफ़ेस (यूआई) के साथ कंपोज़ किया जा सकने वाला एक वीडियो प्लेयर लेआउट हो सकता है. यह लेआउट, दो कॉन्फ़िगरेशन के बीच डाइनैमिक तौर पर स्विच करता है.
अगला प्रिमिटिव जिसके बारे में मुझे बात करनी है वह है 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) }
)
}
}
}इसलिए, ग्रिड 2D लाइन और कॉलम लेआउट को हैंडल करता है. वहीं, FlexBox, जो अब स्टेबल है, एक डाइमेंशन वाले लेआउट को हैंडल करता है. इसके लिए, अब भी स्पेस को समझदारी से डिस्ट्रिब्यूट करने, छोटा करने, और कई लाइनों को रैप करने की ज़रूरत होती है.
यह एक सुविधाजनक और रिस्पॉन्सिव लेआउट है. इसका इस्तेमाल, FlowRow और FlowColumn जैसे दो अलग-अलग कंपोज़ेबल के बजाय किया जा सकता है. ऐसा इसलिए, क्योंकि यह एक सिंगल कंटेनर है, जिसकी दिशा को इसके कॉन्फ़िगरेशन ब्लॉक में डाइनैमिक तौर पर बदला जा सकता है. इसके लिए, चाइल्ड कंपोज़ेबल को फिर से बनाने की ज़रूरत नहीं पड़ती.
साथ ही, यह पूरे CSS FlexBox एल्गोरिदम को लागू करता है. इसलिए, आपके पास रैप, justify-content, align-items, और gap की सुविधा होती है.
हर चाइल्ड एलिमेंट पर, आपके पास Modifier.flex होता है. इससे आपको बेसिस, रिलेटिव ग्रो, श्रिंक फ़ैक्टर, alignSelf वगैरह की जानकारी मिलती है.
फ़्रांसेस्को रोमानो
ठीक है, अब स्क्रीन-लेवल की परिभाषा से ज़ूम आउट करके, ऐप्लिकेशन-लेवल के लेआउट पर चलते हैं. साथ ही, यह देखते हैं कि हम पूरे ऐप्लिकेशन में टॉप-लेवल नेविगेशन और मल्टी-पैन स्क्रीन की व्यवस्था कैसे करते हैं, न कि सिर्फ़ एक स्क्रीन पर.
MediaQuery आपको डीपी में विंडो की सटीक चौड़ाई और ऊंचाई देता है. हालांकि, ऐप्लिकेशन-लेवल के आर्किटेक्चर के लिए, आपको स्टैंडर्ड और तय किए गए व्यूपोर्ट ब्रेकपॉइंट चाहिए होते हैं. ये विंडो साइज़ क्लास होते हैं.
इसका मतलब है कि आपको हर बकेट में मौजूद हर स्क्रीन को रिस्पॉन्सिव बनाना होगा. हालांकि, अलग-अलग बकेट के बीच स्ट्रक्चर में ज़्यादा बदलाव और अपडेट किए जा सकते हैं. साथ ही, उनके बीच अंतर भी हो सकता है.
उदाहरण के लिए, कॉम्पैक्ट स्क्रीन पर सिर्फ़ सूची दिखाकर या बड़ी विंडो में सूची और जानकारी, दोनों दिखाकर.
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...
}ध्यान दें कि यह स्टैंडर्ड MediaQuery के बजाय, derivedMediaQuery का इस्तेमाल कर रहा है.
क्यों?
ऐसा इसलिए, क्योंकि विंडो की चौड़ाई और ऊंचाई की वैल्यू अक्सर बदलती रहती हैं.
जब उपयोगकर्ता आपके ऐप्लिकेशन का साइज़ बदलते हैं, जैसे कि डेस्कटॉप पर, तो ये वैल्यू हर फ़्रेम में बदल सकती हैं.
साथ ही, हो सकता है कि आप हर फ़्रेम पर अपने ऐप्लिकेशन-लेवल के स्कैफ़ोल्ड को फिर से कंपोज़ न करना चाहें. इसके बजाय, ऐसा सिर्फ़ तब करना चाहें, जब विंडो असल में ब्रेकपॉइंट को पार कर जाए.
विंडो साइज़ क्लास तय करने के बाद, ज़्यादा स्पेस होने पर हम अपने ऐप्लिकेशन को सिंगल-पेन फ़्लो से मल्टी-पेन लेआउट में कैसे बदलें?
Jetpack Navigation 3 की यही खासियत है. इसे Compose स्टेट और सीन के हिसाब से डिज़ाइन किया गया है.
सबसे पहले, आपके पास पिछली गतिविधियों का ऐक्सेस होता है. यह NavKeys की एक सूची होती है. इसलिए, यह काफ़ी सुविधाजनक है और इसे स्टेट के तौर पर सेव करना आसान है.
स्टैक में मौजूद हर कुंजी, NavEntry में बदल जाती है. इसमें कंपोज़ेबल और मेटाडेटा शामिल होता है.
NavDisplay, एंट्री को ड्रॉ करने के तरीके को हार्ड-कोड नहीं करता. इसके बजाय, यह सीन की रणनीतियों की सूची का आकलन करता है.
इस रणनीति के तहत, यूज़र इंटरफ़ेस (यूआई) की सुविधाओं की जांच की जा सकती है. उदाहरण के लिए, विंडो साइज़ क्लास.
if (isExpandedWidth) {
/* return a multi-pane scene */
}
else {
/* return a single pane scene */
}पिछली गतिविधियों की जांच की जा सकती है. इसके बाद, यह तय किया जा सकता है कि एक NavEntry को एक ही पैनल में रेंडर करना है या एक साथ कई आइटम रेंडर करने हैं या एक के ऊपर एक आइटम रेंडर करना है.
यह बहुत आसान है.
मल्टी-पेन रणनीति के अलावा, विंडो साइज़ क्लास से यह भी तय होता है कि आपको किन नेविगेशन कॉम्पोनेंट का इस्तेमाल करना है, ताकि स्क्रीन स्पेस का ज़्यादा से ज़्यादा इस्तेमाल किया जा सके और उसे इस्तेमाल करना आसान हो.
उदाहरण के लिए, कम चौड़ाई वाली स्क्रीन पर, आपको पेज या स्क्रीन के नीचे मौजूद नेविगेशन बार चाहिए. वहीं, मीडियम साइज़ की स्क्रीन पर, आपको नेविगेशन रेल चाहिए. इसके अलावा, बड़ी स्क्रीन पर, आपको पूरा नेविगेशन पैनल चाहिए.
ऐप्लिकेशन-लेवल के लेआउट से जुड़े फ़ैसलों के लिए Nav3 और स्क्रीन-लेवल की परिभाषा के लिए रिस्पॉन्सिव कॉम्पोनेंट को जोड़ने पर, आपको ऐसे ऐप्लिकेशन मिलते हैं जो इस तरह दिखते हैं. साथ ही, ये हर विंडो साइज़ और एनवायरमेंट में शानदार तरीके से काम करते हैं.
खास तौर पर Nav 3 के लिए, अगर आपको नए अपडेट के बारे में ज़्यादा जानना है, तो Jetpack Navigation 3 की मदद से टाइप-सेफ़ डीप लिंक बनाने के बारे में जानकारी देने वाला वीडियो देखें. इसमें डीप लिंक के लिए सहायता पाने के बारे में भी बताया गया है.
फ़्रांसेस्को रोमानो
ठीक है, आज हमने कई नए एपीआई के बारे में बात की है: MediaQuery, Grid, FlexBox, Nav3.
इस तरह, Jetpack Compose की मदद से अडैप्टिव ऐप्लिकेशन बनाए जा सकते हैं.
हालांकि, एजेंटिक डेवलपमेंट के दौर में, Jetpack Compose का इस्तेमाल करने का एक और अहम फ़ायदा है: यह ज़्यादा असरदार है.
आपने शायद कीनोट के दौरान सुना होगा कि Compose पर माइग्रेट करने के बाद, ऐप्लिकेशन 30% से भी कम टोकन इस्तेमाल कर रहे हैं. हम Compose ऐप्लिकेशन के लिए, एजेंटिक वर्कफ़्लो को और भी बेहतर बनाने की कोशिश कर रहे हैं.
आज, हम Compose Adaptive स्किल में हुए खास अपडेट के बारे में बताएंगे.
android skills list
android skills add adaptiveजैसा कि मैंने बताया, लेआउट को ग्रिड, फ़्लेक्सबॉक्स, और MediaQuery पर माइग्रेट करने के साथ-साथ, अब यह ऐप्लिकेशन में एंड-टू-एंड स्टेट मैनेजमेंट को साफ़ तौर पर मैनेज करता है. साथ ही, हम असल दुनिया में स्टेट लॉस से जुड़े बग के ख़िलाफ़ इस स्किल की जांच करते हैं: नेविगेशन, स्क्रोल पोज़िशन, ऐक्टिव मीडिया प्लेबैक, आधा टाइप किया गया टेक्स्ट इनपुट वगैरह को सुरक्षित रखना.
इस स्किल में अब फ़ोल्डेबल डिवाइस के पॉस्चर को ऑप्टिमाइज़ करने की सुविधा भी शामिल है. जैसे, मीडिया और कैमरा स्क्रीन को टेबलटॉप मोड में अपने-आप रीफ़ैक्टर करना. हालांकि, जब आपका ऐप्लिकेशन किसी Googlebook या माउस और कीबोर्ड वाले बाहरी मॉनिटर पर चलता है, तो लेआउट सिर्फ़ आधी कहानी होती है.
अडैप्टिव स्किल अब आपके यूज़र इंटरफ़ेस (यूआई) की जांच कर सकती है. इससे कीबोर्ड शॉर्टकट, कस्टम पॉइंटर कर्सर, कर्सर घुमाने पर दिखने वाली स्थितियां, टूलटिप वगैरह को ढूंढने और लागू करने में मदद मिलती है.
आइए, इसे इस्तेमाल करके देखते हैं.
मैंने इसका एक डेमो रिकॉर्ड किया.
इसलिए, हमारे पास हमारा ऐप्लिकेशन है.
हम अपने पालतू जानवरों को देख सकते हैं.
हम उनके साथ मैसेज पर बातचीत कर सकते हैं.
हम उनका एक वीडियो देख सकते हैं.
हां, हम सबसे लोकप्रिय पालतू जानवर देख सकते हैं.
हालांकि, आपको पहले से ही पता है कि यह बहुत अच्छा नहीं है.
अब देखते हैं कि मुझे ऐसी तस्वीर देखनी है जिसमें कुछ ड्रामा हो और बिल्लियां हों.
ठीक है, मुझे इन्हें बड़े साइज़ में देखना है.
अरे, यह तो होम पेज पर वापस जाने के लिए है.
ठीक है, मैं अपने दोस्त को इसके बारे में बताती हूँ.
हां, तो "बडी", हम्म.
ऐप्लिकेशन अच्छा नहीं है.
यह बहुत चौड़ा भी है.
आपको दिखेगा कि यह बहुत चौड़ा है.
चलिए, इसका साइज़ बदलते हैं.
क्या?
हाँ, ठीक है.
मैंने जो टेक्स्ट टाइप किया था वह मिट गया है.
ठीक है, अब Gemini से इसे ठीक करने के लिए कहते हैं.
अडैप्टिव स्किल की मदद से, प्रॉम्प्ट को "ऐप्लिकेशन को अडैप्टिव बनाएं" जितना आसान बनाया जा सकता है.
जैसा कि आपने देखा, मैं Gemini 3.0 Flash का इस्तेमाल कर रहा हूँ. यह स्किल के साथ मिलकर, इस काम को पूरा कर सकता है.
इसने स्किल को चुन लिया है और अब यह बदलावों को लागू करना शुरू कर रहा है.
यह कुछ सवाल पूछेगा, जैसे कि "अरे, MediaQuery एक्सपेरिमेंट के तौर पर उपलब्ध है.
क्या आपको वाकई इसका इस्तेमाल करना है?"
और आगे बढ़ते रहें.
मुझे वीडियो की स्पीड बढ़ानी पड़ी, ताकि मैं कुछ चीज़ें काट सकूं. हालांकि, अगर आप डेमो एरिया में आते हैं, तो खुद ही इन स्किल को आज़मा सकते हैं.
यह उन स्किल में से एक है जिन्हें हमने रिलीज़ किया है.
उदाहरण के लिए, हमारे पास एक नई स्किल भी है. इसकी मदद से, आपको स्टाइलस के शानदार अनुभव देने में मदद मिलेगी.
इसलिए, इसे आज़माने के लिए इस इलाके में आएं.
इसलिए, यह काम पूरा हो गया है.
इसने 11 फ़ाइलों में बदलाव किया.
चलिए, इसे चलाकर देखते हैं.
ठीक है।
ग्रिड पहले से ही अपना काम कर रहा है.
नेविगेशन को साइड में ले जाया गया.
चलिए, फ़िल्टर आज़माते हैं.
मुझे लगता है कि अब ये काम करेंगे.
हां, बिलकुल.
ठीक है, चलिए अपने दोस्त को मैसेज भेजते हैं और बताते हैं कि अब यह ऐप्लिकेशन बहुत अच्छा है.
चलिए, ज़्यादा स्पेस के साथ देखते हैं कि यह कैसा दिखता है.
हां, इसे सूची की जानकारी वाले लेआउट पर माइग्रेट कर दिया गया है.
इस रन में करीब 20 मिनट लगे और 2,00,000 टोकन इस्तेमाल हुए.
ठीक है।
फ़्रांसेस्को रोमानो
आम तौर पर, मैं बिज़नेस मेट्रिक और सफलता की कहानियों से अपनी बात शुरू करता हूं. हालांकि, ADS के लिए, मुझे सीधे कोड पर जाना था.
हालांकि, मैं कुछ मिनटों में यह बताना चाहता हूं कि इंजीनियरिंग टीम ने जो काम किया है उससे हमें क्या फ़ायदा मिला है.
आज के समय में, आपका Android ऐप्लिकेशन 58 करोड़ से ज़्यादा ऐक्टिव बड़ी स्क्रीन वाले डिवाइसों पर चल सकता है. हमें यह भी पता है कि मल्टी-डिवाइस और फ़ोल्ड किए जा सकने वाले डिवाइसों के उपयोगकर्ता, Android पर सबसे ज़्यादा दिलचस्पी दिखाने वाले सेगमेंट में से एक हैं.
आइए, दो केस स्टडी पर नज़र डालते हैं. इनसे पता चलता है कि अडैप्टिव बिल्ड शिप करने पर क्या होता है.
पहला ऐप्लिकेशन Gratitude है. यह खुद की देखभाल करने और रोज़ाना डायरी लिखने के लिए एक लोकप्रिय ऐप्लिकेशन है.
Gratitude ने अडैप्टिव स्क्रीन डिज़ाइन को अपनाकर, टैबलेट और फ़ोल्ड किए जा सकने वाले डिवाइसों को, तेज़ी से बढ़ने वाले उपयोगकर्ता हासिल करने के चैनल के तौर पर स्थापित किया.
सबसे पहले, उन्होंने बड़ी स्क्रीन वाले डिवाइसों पर, सदस्यता लेने वाले लोगों की संख्या को दोगुना किया.
खास तौर पर, टैबलेट और फ़ोल्ड किए जा सकने वाले डिवाइसों से हर दिन होने वाले इंस्टॉल की संख्या में 100% की बढ़ोतरी हुई.
उन्होंने बड़ी स्क्रीन वाले डिवाइसों पर अपने उपयोगकर्ताओं की संख्या को, कुल उपयोगकर्ताओं की संख्या के 7% से बढ़ाकर 10% कर दिया.
दूसरा ऐप्लिकेशन BoxBoxClub है. यह फ़ॉर्मूला 1 और मोटरस्पोर्ट के लिए एक कंपैनियन ऐप्लिकेशन है. फ़ोल्ड किए जा सकने वाले डिवाइसों और बड़ी स्क्रीन वाले डिवाइसों के लिए, अपने ऐप्लिकेशन के बिल्ड को अपडेट करने के बाद, इसके टेलीमेट्री डेटा में काफ़ी बढ़ोतरी देखी गई: सक्रिय उपयोगकर्ताओं की कुल संख्या में 7.6% की बढ़ोतरी हुई और यूज़र ऐक्टिविटी में 54% की बढ़ोतरी हुई.
ठीक है।
हमारा सेशन खत्म होने वाला है. इसलिए, मैं आपको तीन अहम बातें बताना चाहता हूं.
अडैप्टिव ऐप्लिकेशन बनाने से, आपका ऐप्लिकेशन ज़्यादा डिवाइसों और ज़्यादा उपयोगकर्ताओं के लिए उपलब्ध हो पाता है. साथ ही, यह ऐप्लिकेशन को इस बढ़ते नेटवर्क के लिए आने वाले समय में भी काम करने लायक बनाता है.
Android डेवलपमेंट के लिए, Compose का इस्तेमाल सबसे पहले किया जाता है.
सभी नए टूल, लाइब्रेरी, और स्किल, Compose के लिए बनाए गए हैं.
Compose, अडैप्टिव लेआउट और एजेंटिक डेवलपमेंट के लिए बेहतरीन है.
कोड बेस छोटे होते हैं और उन्हें समझना आसान होता है. Compose कोड के साथ काम करते समय, एजेंट कम टोकन इस्तेमाल करते हैं.
इसलिए, आगे बढ़ें और सभी नए टूल, स्किल, और एपीआई आज़माएं. साथ ही, इनका आनंद लें.
धन्यवाद.
<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>mediaQuery { ... }
derivedMediaQuery { ... }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
}@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
}
}@Composable
fun currentVideoPlayerLayout(): VideoPlayerLayout {
return mediaQuery {
when(windowPosture) {
UiMediaScope.Posture.Tabletop -> VideoPlayerLayout.Tabletop
else -> VideoPlayerLayout.Default
}
}
}@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")
}
}
}@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(
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(
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))
}@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)
}
}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()
}
}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)
}
}@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()
}
}
}@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) }
)
}
}
}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...
}if (isExpandedWidth) {
/* return a multi-pane scene */
}
else {
/* return a single pane scene */
}android skills list
android skills add adaptive