תשתית לאפליקציות

התאמת האפליקציה לכל מסך באמצעות פיתוח נייטיב

רמקולים

מידע כללי

מידע על ההרצאה

הסביבה העסקית של Android היא עצומה, והמשתמשים עוברים בצורה חלקה בין טלפונים, טאבלטים, מכשירים מתקפלים וסביבות של מחשבים. עכשיו, כשאנחנו מתמקדים ב-Compose, בסשן הזה נבחן איך לתכנן את האפליקציה לחלונות שאפשר לשנות את הגודל שלהם וליחסי גובה-רוחב שונים – שבהם הנחות מסורתיות לגבי פריסה לא עובדות, מה שמוביל לממשקי משתמש מתוחים ולאובדן מצב.

אנחנו מזמינים אתכם לגלות איך היכולות החדשות ב-Jetpack Compose, ב-Navigation 3 וב-WindowManager יכולות לעזור לכם ליצור פריסות דינמיות שמתאימות באופן טבעי למסכים קומפקטיים וגם לתצוגות עם שתי חלוניות. בסשן נציג אסטרטגיות שיעזרו לכם להאיץ את תהליך הפיתוח באמצעות Gemini והמיומנויות שלנו, כמו המיומנות האדפטיבית, כדי לשכתב באופן אוטומטי בסיסי קוד מדור קודם למסכים גדולים יותר.

סיכום

  • בפלטפורמות Android נדרשות אפליקציות מותאמות כי המערכת מתעלמת מנעילות של כיוון ויחס גובה-רוחב במסכים גדולים יותר החל מ-Android 16, והאפשרויות לביטול ההסכמה במניפסט הוסרו ב-Android 17.
  • ה-API הניסיוני MediaQuery עוקב באופן הצהרתי אחרי יכולות פעילות של ממשק המשתמש, כמו גודל החלון ותנוחות הקיפול. הוא מחליף את החיבור של כמה מנהלי פלטפורמות מדור קודם למצב Compose.
  • רכיבי פרימיטיב יציבים של Grid ו-FlexBox מספקים אבני בניין לפריסה מותאמת. ‫Grid מאפשר סידורים רב-ממדיים עם פרמטרים בתוך רכיב Composable אחד, ו-FlexBox מספק חלוקת שטח חד-ממדית וגלישת טקסט.
  • ‫Jetpack Navigation 3 מארגן תהליכים עם כמה חלוניות ברמת האפליקציה לפי מצב וסצנות. אסטרטגיות של סצנות בהתאמה אישית מעריכות את היכולות של ממשק המשתמש ואת סוגי הגודל של החלונות כדי לעבור באופן דינמי בין מצגות עם חלונית אחת למצגות עם כמה חלוניות.
  • הסקילים של Compose Adaptive ל-Gemini הופכים את תהליך ההעברה של אפליקציות לרכיבים רספונסיביים מודרניים לאוטומטי. היכולת המעודכנת משנה את פריסות הממשק, פותרת באגים שקשורים לאובדן מצב ומגדירה את אמצעי הקלט במחשב, כמו מקשי קיצור וסמנים.

משאבים

Gemini Notebook

נושאים

  • jetpack-compose
  • פריסות-מותאמות
  • mediaquery
  • grid-layout
  • flexbox
  • navigation-3
  • android-skills

תמליל

הורדת התמליל

מבוא להתאמת אפליקציות למסכים שונים

  • פרנצ'סקו רומנו

    שלום לכולם,

  • היום נתעמק בהיבטים הטכניים של התאמת האפליקציה לכל מסך באמצעות Jetpack Compose.

  • למה כדאי להתאים את המודעות לכל מסך?

  • כי הסביבה העסקית של Android התרחבה וכוללת הרבה סוגי מכשירים שונים.

  • קובץ ה-APK יכול לפעול בטלפונים, במכשירים מתקפלים, בטאבלטים, במחשבי Chromebook ו-Googlebook, בטלוויזיות, במערכות מולטימדיה לרכב, במכשירי XR ועוד.

  • אז הדבר הראשון הוא לפגוש את המשתמשים במקום שבו הם נמצאים.

  • השינוי השני הוא המעבר של פלטפורמת Android לאפליקציות מותאמות, וזה לא שינוי זמני.

  • זה לא שאנחנו חוזרים לעולם שבו יש רק טלפונים, זה כאן כדי להישאר.

  • ויש לי כאן ציר זמן קצר.

  • בשנת 2022 התחלנו להניח את היסודות.

  • השקנו את Android 12L, הגרסה הראשונה של Jetpack WindowManager, נתנו לכם ממשקי API כמו קטגוריות של גודל חלון ופרסמנו את הגרסה הראשונה של הנחיות האיכות לאפליקציות אדפטיביות.

  • בנוסף, עדכנו את Google Play כך שהדירוגים מחולקים לפי סוג המכשיר, והתחלנו לכלול את הנחיות האיכות לאפליקציות מותאמות בהחלטות לגבי הדירוג.

  • ב-Android 15, הוספנו גם את ממשק המחשב ל-AOSP, ואת Compose Material 3 Adaptive שהגיע לגרסה יציבה.

  • החל מגרסת Android 16, ‏SDK 36, המערכת פשוט מתעלמת מנעילות כיוון, נעילות של יחס גובה-רוחב ודגלים של שינוי גודל במסכים גדולים יותר.

  • יחד עם ההשקה של מסכים מחוברים, עכשיו אפשר להפעיל את ה-APK כמעט בכל גודל חלון.

  • ב-SDK 36 היה דגל מניפסט זמני אחד לביטול ההתאמה של הכיוון ויחס הגובה-רוחב.

  • ב-Android 17, האפשרות הזו לא קיימת.

  • לכן, פיתוח רספונסיבי הוא דרישה בסיסית של הפלטפורמה.

  • בעשור האחרון עבדנו בשיתוף פעולה הדוק עם צוותים בכל הסביבה העסקית, ולמדנו ממקור ראשון למה פיתוח של פריסה דינמית יכול להיות קשה.

  • יש לכם אובדן מצב, ממשקי API של חלונות מקוטעים, אותות לא ברורים לגבי מתי להחליף פריסה, ופשוט כמה הנחות ישנות שמוטמעות עמוק בבסיס הקוד.

  • אז לקחנו את כל מה שלמדנו מהאתגרים של המפתחים בעשור הזה, והטמענו את זה ישירות בשני מקומות: קודם כל, בערכת הכלים של ממשק המשתמש עצמו.

  • היום נבחן כמה יכולות חדשות וחשובות ב-Jetpack Compose: שאילתת מדיה, רשת, FlexBox ו-Navigation 3.

  • אבל גם בכלים אג'נטיים שכותבים את הקוד יחד איתכם, באמצעות המיומנויות הרשמיות של Android ל-Gemini ב-Android Studio וב-Android CLI.

  • לדוגמה, נראה את המיומנויות המותאמות של Compose שהושקו ב-Google I/O מוקדם יותר השנה.

  • כבר הייתה לו יכולת להעביר פריסות של טלפונים לגדלים גדולים יותר של חלונות.

  • עכשיו הוא יכול גם לתקן את אובדן ערך דינמי, להטמיע חוויות ייחודיות למכשירים מתקפלים כמו על משטח, מסך למעלה, ולשפר את התמיכה בקלט על ידי גילוי והטמעה של מקשי קיצור, סמנים בהתאמה אישית, הסברים קצרים ועוד.

  • אוקיי, נתחיל עם Jetpack Compose, שהוא הבסיס לכל זה.

  • הגרסה היציבה של Jetpack Compose הושקה ב-2021, אז השנה אנחנו חוגגים את יום ההולדת החמישי שלה.

  • במהלך חמש השנים האלה, ראינו באלפי אפליקציות בסביבת הייצור עד כמה קל ואמין יותר ליצור פריסות דינמיות באמצעות Compose.

  • פיתוח ל-Android מתבצע עכשיו באופן רשמי קודם כל ב-Compose.

  • כלומר, כל הפריסות, העיצובים ורכיבי הניווט החדשים והמודרניים שמותאמים למכשיר נוצרו בשביל Compose.

  • מנקודת המבט של ארכיטקטורת המערכת, היתרון הכי גדול של פיתוח נייטיב לאפליקציות רספונסיביות על פני מערכת התצוגה הקודמת הוא האינטראקציה בין המצב לבין ממשק המשתמש.

  • כדי להסביר את זה, אני רוצה לעשות השוואה מהירה עם מערכת התצוגה.

  • אז בואו נחשוב איך מערכת התצוגה עבדה.

  • ממשק המשתמש היה עץ של אובייקטים ניתנים לשינוי, שהרחבתם פעם אחת מקובצי XML סטטיים.

  • היו לך מגדירי משאבים שונים, ובואו נתחיל עם הבעיה העיקרית כאן: כפילות בין מגדירי משאבים שונים.

  • כשיוצרים ממשק משתמש מותאם ב-XML, אין לוגיקה מותנית בתוך קובץ הפריסה.

  • במקום זאת, פשוט משכפלים את כל ההיררכיה בתיקיות המשאבים, כמו פריסה, פריסה של החלון הקטן ביותר של 600dp, פריסה לרוחב וכן הלאה.

  • אוקיי, תסתכלו על הדוגמה של 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 בפריסה לטלפון, שום דבר בקומפיילר לא מחייב לעדכן אותו גם בפריסה לטאבלט.

  • ובגלל שהרכיבים מופרדים בקבצים שונים, כמעט בטוח שעם הזמן ייווצר חוסר התאמה במבנה.

  • השכפול הזה קשור גם לבעיה אחרת, שהיא אובדן המצב במהלך שינויים בהגדרות.

  • חשוב לזכור שבמערכת View, הרכיבים של ממשק המשתמש הם בעלי מצב (stateful).

  • ‫EditText הוא הבעלים של מאגר הנתונים הפנימי שניתן לעריכה, מיקום הסמן בהתאמה אישית, מצב המיקוד ומצבים נוספים.

  • לכן, כשמשתמש, נניח, פותח את המכשיר או משנה את גודל החלון מעבר לסף מסוים, Android הורסת את כל היררכיית התצוגות ומנפחת את קובצי ה-XML החדשים מאפס.

  • כדי לשמור את הקלט של המשתמש, ה-framework מסתמך על onSaveInstanceState, שיארוז את המצב שמקושר למזהה של התצוגה הזו.

  • אבל זה יוצר שלושה מצבי כשל אפשריים.

  • קודם כול, אם מישהו נתן בטעות למזהה את השם text_input במקום text_msg בקובץ ה-XML האחר, שחזור המצב מבטל את טקסט הקלט של המשתמש בלי להציג אזהרות.

  • שנית, onRestoreInstanceState פועל אחרי onViewCreated, מה שעלול להתנגש לעיתים קרובות עם משקיפי ViewModel ולגרום ללולאות של קריאה חוזרת (callback).

  • שלישית, אם מחליפים את סוג הווידג'ט בין מגדירים, נניח שבמסך קומפקטי רוצים ספינר ובמסך רחב יותר רוצים RadioGroup, אם יש להם אותו מזהה, זה קורס בזמן ריצה עם ClassCastException.

  • לעומת זאת, ב-Jetpack Compose, ממשק המשתמש הוא לא אובייקט שיוצרים פעם אחת.

  • ממשק המשתמש הוא פונקציה טהורה של מצב.

  • וזה משנה לגמרי את המשמעות של שינוי בהגדרות.

  • מהו שינוי בהגדרות האישיות ב-Compose?

  • יש לי רמז.

טיפול בשינויים בהגדרות וביכולות של ממשק המשתמש

  • פרנצ'סקו רומנו

    כן.‏

  • ב-Compose, שינוי בהגדרות הוא רק עוד קומפוזיציה מחדש, וקומפוזיציות מחדש מתרחשות כל הזמן.

  • כשהחלון מתרחב, למשל כי המשתמש פתח את המכשיר, הפונקציות הניתנות לקישור קוראות את הגודל החדש, והפונקציות שהפלט שלהן תלוי בגודל פשוט פועלות שוב.

  • זה כל האירוע.

  • אבל עכשיו, בואו נראה מה המשמעות של כתיבת אפליקציות מודרניות ורספונסיביות באמצעות הכלים והרכיבים העדכניים ביותר של פיתוח נייטיב.

  • לכן, בעולם ה-XML הייתם צריכים לשאול שאלות כמו "האם אני פועל בטלפון, בטאבלט או במחשב?", ואז להסתעף בממשק המשתמש על סמך קטגוריית המכשיר.

  • אבל יש סיבה בסיסית לכך שלא כדאי לעשות את זה.

  • גם כשהאפליקציה פועלת באותו מכשיר פיזי, ההקשר של האפליקציה והיכולות של ממשק המשתמש יכולים להשתנות באופן דינמי בזמן הריצה.

  • אז המסר שאני רוצה להעביר הוא שהחלטות לגבי ממשק המשתמש צריכות להתבסס על היכולות הפעילות הנוכחיות: גודל החלון הנוכחי, המנח, מאפייני חומרה מסוימים.

  • בואו נראה דוגמה קלאסית: ממשק מחשב במסך חיצוני.

  • אם מחברים את טלפון Pixel לצג חיצוני על השולחן, אותו תהליך של האפליקציה שפעל במסך לאורך בגודל 6 אינץ' פועל עכשיו בצג רחב בגודל 32 אינץ' עם מקלדת פיזית ועכבר.

  • לכן, בדיקה של סוג המכשיר שהאפליקציה פועלת בו לא מבטיחה שום דבר לגבי המסך שבו היא מוצגת בפועל, או לגבי אופני הקלט.

  • בנוסף, Android תומכת בריבוי חלונות, כך שאפשר לשנות את הגודל של החלונות.

  • גם אם המסך של Googlebook גדול יותר מהטלפון, יכול להיות שהאפליקציה פועלת בחלון ברוחב 400dp, יחד עם אפליקציות אחרות.

  • לכן, גם שאילתות לגבי המדדים הסטטיים של המסך הפיזי יכולות להיות חסרות משמעות.

  • לכן, כדי לתכנן אפליקציות ל-Compose, קודם כל צריך להבין איך לשלוח שאילתות לסביבה.

  • ואני רוצה להתחיל בניסיון להגדיר את הקונספט של יכולות ממשק המשתמש.

  • היכולות של ממשק המשתמש כוללות לא רק את הרוחב והגובה של החלון או את המצב של המכשיר המתקפל, אלא גם את אופני הקלט והפלט שפעילים כרגע.

  • ראינו שבסביבת המחשב, המשתמשים לא תמיד משתמשים במסך מגע.

  • הם יכולים להשתמש במקלדת ובעכבר.

  • במערכת של הרכב, בחלק מהמכוניות יש כפתור סיבובי או קלט אחר, דרך אחרת להזין אותות.

  • בנוסף, מעבר לגודל המסך ולקלט, המערכת בודקת את המרחק של המשתמש מהמסך, כמו מרחק הצפייה, או אם מצלמה או מיקרופון מחוברים כרגע.

  • אלה כל היכולות של ממשק המשתמש.

זיהוי תנוחות באמצעות mediaQuery API

  • פרנצ'סקו רומנו

    עד עכשיו, כדי לאחזר את כל האותות, הייתם צריכים לחבר יחד 5 או 6 ממשקי API שונים ממקורות שונים.

  • הייתם צריכים את אובייקט ההגדרות האישיות, את WindowMetricsCalculator, את WindowInfoTracker, את InputManager, את PackageManager, ואז להמיר כל אחד מהם באופן ידני למצב, מצב של פיתוח נייטיב.

  • נהנית לעשות את זה?

  • לא.

  • כדי לפתור את הבעיה, הוספנו את MediaQuery API ב-androidx.compose.ui.

    mediaQuery { ... }
    derivedMediaQuery { ... }
  • הוא מבוסס על שאילתות מדיה של CSS.

  • והמילה "מדיה" לא מתייחסת להפעלת אודיו או וידאו.

  • הוא מתייחס לאמצעי, לסביבה שבה ממשק המשתמש מוצג.

  • ‫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 ב-Dp.

  • windowPosture, שכרגע מיועד למכשירים מתקפלים, ויכול להיות שטוח, על משטח עם מסך למעלה או כמו ספר.

  • הערך pointerPrecision מציין את רמת הדיוק של אמצעי ההצבעה הראשי: Fine (עכבר, סטיילוס או משטח מגע), Coarse (מסך מגע), Blunt (ג'ויסטיק, בקרים) או None.

  • ‫keyboardKind יכול להיות פיזי, וירטואלי או ללא.

  • המאפיינים hasCamera ו-hasMicrophone עוקבים באופן דינמי אחרי הזמינות של החומרה הזו.

  • אפשר להשתמש במצלמה חיצונית שאפשר לחבר למחשב.

  • והערך viewingDistance מדווח על קרוב, בינוני או רחוק.

  • אבל אני חושב שיהיה קל יותר אם נסתכל על דוגמה אמיתית, שהיא זיהוי אם המכשיר נמצא במצב שולחני.

  • מצב שולחני הוא מצב שבו המשתמשים מקפלים את המכשיר לחצי ובדרך כלל מניחים אותו על משטח.

  • הוא גם מעולה להפעלת סרטונים ארוכים, לשיחות וידאו ולצילום ב-Time Lapse.

  • הוא גם שימושי מאוד, למשל, כדי לעקוב אחרי מתכון או לצפות במצגת.

  • בעבר, כדי לזהות את מצב השולחן לפני 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 מקבלת פרמטר למבדה יחיד, ובתוך היקף המקלט של הלמבדה הזו, היא חושפת את קבוצת המאפיינים שראינו עכשיו.

  • בדוגמה הזו, פשוט על ידי בדיקת windowPosture בביטוי when, אנחנו יכולים להחזיר מיד את ה-composable הנכון.

  • הנה שתי דוגמאות נוספות לשימוש ב-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")
            }
        }
    }
  • הראשון הוא רמת הפירוט.

  • לכן, כשמשתמשים במחשב נייד או במסך של מחשב שולחני עם עכבר, ודיוק הסמן הוא "מדויק", אנחנו יכולים להקטין את המרווחים ברכיבים של ממשק המשתמש. בדוגמה הזו, מ-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.

  • כך אפשר פשוט לספק UiMediaScope מזויף דרך CompositionLocalProvider בתצוגה המקדימה, ולא צריך להפעיל מכשירים מתקפלים ולהגדיר 90° או לחבר עכבר רק כדי להריץ בדיקת ממשק משתמש של Compose.

פריסות מסך רספונסיביות עם Grid ו-FlexBox

  • פרנצ'סקו רומנו

    אוקיי, עכשיו אנחנו יודעים איך לשלוח שאילתות לגבי היכולות של ממשק המשתמש.

  • עכשיו נראה איך יוצרים את הפריסה בפועל.

  • לשם כך, אני רוצה להציג את Grid, פריסת פרימיטיב חדשה ב-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, כשמוסיפים צאצא בלי משתנה, הוא עובר אוטומטית לתא הבא.

    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 היא התא בפינה הימנית העליונה.

  • הוא תומך בהוספה לאינדקס שלילי, כך ששורה -1 היא השורה האחרונה וכן הלאה.

  • אפשר להשתמש ב-rowSpan כדי לפרוס פריט על פני אזור דו-ממדי, ואז יש גם חפיפה.

  • לכן, אם שני פריטים ממוקמים בתאים חופפים, Grid מערום אותם זה על גבי זה בתוך הגבולות האלה, וסדר ה-Z נקבע לפי סדר הקומפוזיציה.

  • לכן, פריטים מאוחרים יותר יופיעו מעל הפריטים האחרים, בדיוק כמו ב-Box.

  • עכשיו אנחנו מכירים את העקרונות הבסיסיים של Grid.

  • נבחן את הדוגמה של נגן הווידאו.

  • בהתאם למצב של המכשיר (פתוח לגמרי או חצי פתוח), אנחנו צריכים לעבור בין פריסת ברירת המחדל לפריסת השולחן.

  • לפני 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()
           }
       }
    }
  • עכשיו אנחנו יכולים ליצור פריסה אחת של נגן וידאו שניתנת לשילוב עם ממשק המשתמש, שעוברת באופן דינמי בין שתי הגדרות אישיות.

  • הפרימיטיב הבא שאני רוצה לדבר עליו הוא 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 מטפל בפריסות של שורות ועמודות דו-ממדיות, FlexBox, שגם הוא יציב עכשיו, מטפל בפריסות חד-ממדיות שעדיין דורשות חלוקת שטח חכמה, כיווץ וגלישת טקסט בכמה שורות.

  • זהו פריסה נוחה ורספונסיבית שאפשר להשתמש בה במקום שני רכיבים נפרדים, כמו FlowRow ו-FlowColumn, כי זהו מאגר יחיד שהכיוון שלו יכול להשתנות באופן דינמי בתוך בלוק ההגדרות שלו בלי ליצור מחדש רכיבים צאצאים.

  • הוא גם מיישם את האלגוריתם המלא של CSS FlexBox, אז יש לכם wrap,‏ justify-content,‏ align-items ו-gap.

  • ובכל צאצא, יש לכם את Modifier.flex שנותן לכם בסיס, גדילה יחסית, גורמי כיווץ, alignSelf וכן הלאה.

ארכיטקטורת פריסה ברמת האפליקציה ו-Navigation 3

  • פרנצ'סקו רומנו

    אוקיי, עכשיו נתרחק מההגדרה ברמת המסך לפריסות ברמת האפליקציה, איך אנחנו בונים את הניווט ברמה העליונה ואת סידורי המסכים עם כמה חלוניות בכל האפליקציה, ולא רק במסך הבודד.

  • ‫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 הרגיל.

  • למה?

  • כי הרוחב והגובה של החלון הם ערכים שמשתנים לעיתים קרובות.

  • כשמשתמשים משנים את גודל האפליקציה, למשל במחשב, הערכים האלה יכולים להשתנות בכל פריים.

  • סביר להניח שאתם לא רוצים ליצור מחדש את ה-scaffold ברמת האפליקציה בכל פריים, אלא רק כשהחלון חוצה נקודת עצירה (breakpoint).

  • ואחרי שיש לנו את מחלקות הגודל של החלון, איך אנחנו מעבירים את האפליקציה שלנו מתהליך של חלונית אחת לפריסות של כמה חלוניות כשיש לנו יותר מקום?

  • כאן Jetpack Navigation 3 נכנס לתמונה, כי Nav 3 תוכנן מהיסוד סביב המצב והסצנות של Compose.

  • קודם כול, מקבץ הפעילויות הקודמות (back stack) הוא בבעלותכם. זו פשוט רשימה של NavKeys, אז היא מאוד גמישה וקל לשמור אותה כמצב.

  • כל מפתח במחסנית נפתר ל-NavEntry שמכיל את ה-composable והמטא-נתונים שלו.

  • בנוסף, NavDisplay לא מקודד בתוך הקוד את אופן השרטוט של הרשומות, אלא מעריך רשימה של אסטרטגיות סצנה.

  • במסגרת האסטרטגיה, אתם יכולים לבדוק את היכולות של ממשק המשתמש, למשל, את סוגי הגודל של החלון.

    if (isExpandedWidth) {
        /* return a multi-pane scene */
    }
    else {
        /* return a single pane scene */
    }
  • אתם יכולים לבדוק את מקבץ הפעילויות הקודמות (back stack) ואז להחליט אם להציג NavEntry אחד בחלונית אחת, או כמה פריטים בו-זמנית, או אחד מעל השני.

  • היא מאוד גמישה.

  • בנוסף לאסטרטגיית החלוניות המרובות, קטגוריות גודל החלון גם קובעות באילו רכיבי ניווט כדאי להשתמש כדי למקסם את הארגונומיה ואת השימוש בשטח המסך.

  • לדוגמה, ברוחב קומפקטי, כדאי להשתמש בסרגל ניווט תחתון, ברוחב בינוני, ב-NavRail, וברוחב מורחב, בחלונית הזזה מלאה לניווט.

  • כשמשלבים את Nav3 להחלטות לגבי פריסה ברמת האפליקציה ורכיבים רספונסיביים להגדרה ברמת המסך, מקבלים אפליקציות שנראות ככה ומתאימות כמעט לכל גודל חלון וסביבה.

  • אם אתם רוצים לקבל מידע נוסף על העדכונים האחרונים ב-Nav 3, כולל התמיכה בקישורי עומק, כדאי לצפות בהרצאה Building Type-Safe Deep Links with Jetpack Navigation 3.

הדגמה של פיתוח ואוטומציה אג'נטיים

  • פרנצ'סקו רומנו

    אוקיי, סקרנו היום הרבה ממשקי API חדשים: MediaQuery, ‏ Grid,‏ FlexBox, ‏ Nav3.

  • זה הכול לגבי האופן שבו Jetpack Compose עוזר לכם לפתח אפליקציות מותאמות אישית.

  • אבל בעידן של פיתוח מבוסס-סוכנים, יש עוד יתרון חשוב מאוד לשימוש ב-Jetpack Compose: הוא הרבה יותר יעיל.

  • בטח שמעתם את זה בנאום המרכזי, ראינו אפליקציות שמשתמשות ב-30% פחות טוקנים אחרי המעבר ל-Compose, ואנחנו פועלים כדי לשפר עוד יותר את Agentic Workflows באפליקציות Compose.

  • היום אני רוצה להדגיש את העדכונים העיקריים במיומנות Compose Adaptive.

    android skills list
    android skills add adaptive
  • כמו שאמרתי, בנוסף להעברת הפריסות ל-Grid, ל-Flexbox ול-MediaQuery, עכשיו הוא מטפל באופן מפורש בניהול המצב מקצה לקצה באפליקציה, ואנחנו בודקים את המיומנות מול באגים של אובדן מצב בעולם האמיתי: שמירה על הניווט, מיקום הגלילה, הפעלת מדיה פעילה, קלט טקסט שהוקלד חלקית וכן הלאה.

  • היכולת הזו כוללת עכשיו גם אופטימיזציה של תנוחת המכשיר המתקפל, כמו שינוי אוטומטי של מבנה המסכים של המדיה והמצלמה למצב שולחני. אבל גם כשהאפליקציה פועלת ב-Googlebook או במסך חיצוני עם עכבר ומקלדת, הפריסה היא רק חלק מהסיפור.

  • היכולת "התאמה" יכולה עכשיו לבדוק את ממשק המשתמש כדי למצוא ולהטמיע מקשי קיצור, סמני עכבר בהתאמה אישית, מצבי העברת עכבר, הסברים על כלים ועוד.

  • בואו נראה איך זה עובד.

  • הקלטתי הדגמה של זה.

  • אז יש לנו את האפליקציה.

  • אנחנו יכולים להסתכל על חיות המחמד שלנו.

  • אנחנו יכולים להתכתב איתם.

  • אנחנו יכולים לראות סרטון שלהם.

  • כן, אנחנו יכולים לראות את חיות המחמד המובילות.

  • אבל כבר אפשר לראות שזה לא ממש טוב.

  • עכשיו נראה, אני רוצה לחפש תמונה עם קצת דרמה וחתולים.

  • אוקיי, אני רוצה לראות אותן בגודל גדול יותר.

  • אוקיי, זה מחזיר אותי לדף הבית.

  • אוקיי, אני אספר על זה לחבר שלי.

  • כן, אז, "באדי", הממ.

  • האפליקציה לא טובה.

  • הוא גם רחב מדי.

  • אתם רואים שהיא רחבה מדי.

  • אפשר לנסות לשנות את הגודל שלה.

  • פריט הגרפיקה

  • אה, כן, בסדר.

  • והטקסט שהקלדתי אבד.

  • עכשיו אפשר לבקש מ-Gemini לתקן את זה.

  • בזכות הסקיל "התאמה", אפשר להשתמש בהנחיה פשוטה כמו "התאם את האפליקציה".

  • כמו שאתם רואים, אני משתמש ב-Gemini 3.0 Flash, שיכול לעשות את זה יחד עם המיומנות.

  • הוא קלט את המיומנות, ועכשיו הוא מתחיל להריץ את השינויים.

  • הוא ישאל כמה שאלות, כמו "אוי, MediaQuery הוא ניסיוני.

  • בטוח שברצונך להשתמש בו?"

  • וממשיכים.

  • הייתי צריך להאיץ את הסרטון כדי לחתוך דברים, אבל אם תגיעו לאזור ההדגמה, תוכלו לנסות את המיומנויות בעצמכם.

  • וזו רק אחת מהמיומנויות שהשקנו.

  • לדוגמה, יש לנו גם מיומנות חדשה שתעזור לכם ליצור חוויות מדהימות עם סטיילוס.

  • אז בואו לאזור כדי לנסות.

  • זהו, סיימנו.

  • הוא שינה 11 קבצים.

  • בואו נראה איך זה עובד.

  • אוקיי.

  • ‫Grid כבר עושה את העבודה.

  • תפריט הניווט עבר לצד.

  • אפשר לנסות את המסננים.

  • אני חושב שהם יעבדו עכשיו.

  • כן, הם עושים את זה.

  • אוקיי, בוא נשלח הודעה לחבר שלי ונגיד לו שהאפליקציה מעולה עכשיו.

  • בואו נראה איך זה נראה עם יותר מקום.

  • כן, הוא הועבר לפריסה של רשימה ופרטים.

  • ההרצה הזו נמשכה כ-20 דקות וכללה כ-200,000 טוקנים.

  • אוקיי.

החזר ה-ROI של אפליקציות רספונסיביות ומסקנות עיקריות

  • פרנצ'סקו רומנו

    בדרך כלל אני מתחיל הרצאה עם מדדים עסקיים וסיפורי הצלחה, אבל לגבי ADS, רציתי לקפוץ ישר לקוד.

  • אבל אני רוצה להקדיש כמה דקות כדי לשתף את ההחזר על ההשקעה בהנדסה.

  • היום, אפליקציית Android שלכם יכולה לפעול ביותר מ-580 מיליון מכשירים פעילים עם מסך גדול, ואנחנו גם יודעים שהמשתמשים במכשירים מתקפלים ובמכשירים עם התאמה רחבה הם בין הפלחים הכי מעורבים ב-Android.

  • בואו נבחן שני מקרים לדוגמה שמוכיחים מה קורה כשמשיקים גרסה מותאמת.

  • הראשונה היא Gratitude, אפליקציה פופולרית לטיפול עצמי ולכתיבת יומן יומי.

  • הודות לעיצובים מותאמים למסכים, אפליקציית Gratitude הפכה את הטאבלטים והמכשירים המתקפלים לערוץ צירוף משתמשים עם צמיחה גבוהה.

  • קודם כול, הם הכפילו את קצב צירוף המשתמשים במסכים גדולים.

  • מספר ההתקנות היומיות שמקורן בטאבלטים ובמכשירים מתקפלים גדל ב-100%.

  • הם הגדילו את נתח בסיס המשתמשים במסכים גדולים מ-7% ל-10% מכלל ההתקנות.

  • השנייה היא BoxBoxClub, אפליקציה נלווית לפורמולה 1 ולספורט מוטורי. אחרי שהם עדכנו את ה-build שלהם למכשירים מתקפלים ולמסכים גדולים יותר, נתוני הטלמטריה שלהם הראו רווחים עצומים: עלייה של 7.6% במספר המשתמשים הפעילים הכולל ועלייה של 54% בזמן ההתעניינות.

  • אוקיי.

  • הגענו לסוף השיחה, ואני רוצה לתת לכם שלוש מסקנות חשובות.

  • פיתוח של אפליקציה רספונסיבית מאפשר להציג אותה ביותר מכשירים ולמשתמשים רבים יותר, וגם להכין אותה לעתיד בסביבה המתרחבת הזו.

  • פיתוח ל-Android מתבצע קודם כל ב-Compose.

  • כל הכלים, הספריות והמיומנויות החדשים מיועדים ל-Compose.

  • ‫Compose הוא כלי מעולה לפריסות רספונסיביות ולפיתוח מבוסס-סוכנים.

  • בסיסי הקוד קטנים יותר וקל יותר להבין אותם, והסוכנים משתמשים בפחות אסימונים כשהם עובדים עם קוד Compose.

  • אז קדימה, נסו את כל הכלים, המיומנויות וממשקי ה-API החדשים, ותיהנו.

  • תודה.

קוד

קטעי קוד

קטע קוד XML בדקה 05:17 בסרטון "מבוא להתאמת אפליקציות למסכים שונים"
<LinearLayout android:orientation="vertical">
  <EditText
      android:id="@+id/text_msg"
      android:inputType="textCapCharacters"
      android:maxLength="12" />
</LinearLayout>
קטע קוד XML בדקה 05:17 בסרטון "מבוא להתאמת אפליקציות למסכים שונים"
<ConstraintLayout>
  <TextInputEditText
    android:id="@+id/text_input"
    android:inputType="text" />
</ConstraintLayout>
קטע קוד ב-Kotlin בדקה 11:31 בסרטון Detecting postures with the mediaQuery API
mediaQuery { ... }
derivedMediaQuery { ... }
קטע קוד ב-Kotlin בדקה 11:57 בסרטון "זיהוי תנוחות באמצעות mediaQuery API"
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
}
קטע קוד ב-Kotlin בדקה 13:11 בסרטון "זיהוי תנוחות באמצעות mediaQuery API"
@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
   }
}
קטע קוד ב-Kotlin בדקה 13:42 בסרטון "זיהוי תנוחות באמצעות ה-API של mediaQuery"
@Composable
fun currentVideoPlayerLayout(): VideoPlayerLayout {
   return mediaQuery {
       when(windowPosture) {
           UiMediaScope.Posture.Tabletop -> VideoPlayerLayout.Tabletop
           else -> VideoPlayerLayout.Default
       }
   }
}
קטע קוד ב-Kotlin בדקה 14:07 בסרטון "זיהוי תנוחות באמצעות ה-API של mediaQuery"
@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")
        }
    }
}
קטע קוד ב-Kotlin בדקה 14:59 בסרטון Detecting postures with the mediaQuery API
@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()
    }
}
קטע קוד ב-Kotlin בדקה 15:40 בסרטון "פריסות מסך רספונסיביות באמצעות Grid ו-FlexBox"
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 בדקה 16:50 בסרטון "פריסות מסך רספונסיביות באמצעות Grid ו-FlexBox"
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))
}
קטע קוד ב-Kotlin בדקה 17:59 בסרטון Responsive screen layouts with Grid and FlexBox
@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)
   }
}
קטע קוד ב-Kotlin בדקה 18:51 בסרטון "פריסות מסך רספונסיביות באמצעות Grid ו-FlexBox"
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()
   }
}
קטע קוד ב-Kotlin בדקה 19:20 בסרטון "פריסות מסך רספונסיביות באמצעות Grid ו-FlexBox"
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)
   }
}
קטע קוד ב-Kotlin בדקה 20:00 בסרטון "פריסות מסך רספונסיביות באמצעות Grid ו-FlexBox"
@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()
       }
   }
}
קטע קוד ב-Kotlin בדקה 20:16 בסרטון "פריסות מסך רספונסיביות באמצעות Grid ו-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) }
            )
        }
    }
}
קטע קוד ב-Kotlin בדקה 22:09 בסרטון "App-level layout architecture and Navigation 3"
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 בדקה 23:33 בסרטון "App-level layout architecture and Navigation 3"
if (isExpandedWidth) {
    /* return a multi-pane scene */
}
else {
    /* return a single pane scene */
}
קטע קוד של Shell בדקה 25:18 בסרטון "Agentic development and automation demo"
android skills list
android skills add adaptive