Jetpack Compose היא ערכת כלים מודרנית ודקלרטיבית לבניית ממשקי משתמש ב-Android. Compose מפשטת את הכתיבה והתחזוקה של ממשק המשתמש של האפליקציה באמצעות API הצהרתי שמאפשר לכם לעבד את ממשק המשתמש של האפליקציה בלי לשנות את תצוגות החזית באופן אימפרטיבי. המינוח הזה דורש הסבר, אבל ההשלכות שלו חשובות לעיצוב האפליקציה.
פרדיגמת התכנות הדקלרטיבי
בעבר, היררכיית תצוגות ב-Android הייתה ניתנת לייצוג כעץ של ווידג'טים של ממשק משתמש. כשהמצב של האפליקציה משתנה בגלל פעולות כמו אינטראקציות של משתמשים, צריך לעדכן את ההיררכיה של ממשק המשתמש כדי להציג את הנתונים הנוכחיים.
הדרך הנפוצה ביותר לעדכן את ממשק המשתמש היא לעבור על העץ באמצעות פונקציות כמו findViewById(), ולשנות צמתים באמצעות קריאה לפונקציות כמו button.setText(String), container.addChild(View) או img.setImageBitmap(Bitmap). השיטות האלה משנות את המצב הפנימי של הווידג'ט.
שינוי התצוגות באופן ידני מגדיל את הסבירות לשגיאות. אם חלק מהנתונים מוצג בכמה מקומות, יכול להיות שתשכחו לעדכן את אחד מהתצוגות שבהן הוא מופיע. זה יכול גם להוביל למצבים לא חוקיים, כששני עדכונים מתנגשים בצורה לא צפויה. לדוגמה, יכול להיות שעדכון ינסה להגדיר ערך של צומת שהוסר ממשק המשתמש. באופן כללי, מורכבות תחזוקת התוכנה גדלה עם מספר התצוגות שנדרש לעדכן.
בשנים האחרונות, כל התעשייה החלה לעבור למודל של ממשק משתמש הצהרתי. המודל הזה מפשט את ההנדסה שקשורה ליצירה ולעדכון של ממשקי משתמש. הטכניקה הזו פועלת על ידי יצירה מחדש של כל המסך מאפס, ואז החלת השינויים הנדרשים בלבד. הגישה הזו מונעת את המורכבות של עדכון ידני של היררכיית תצוגה עם שמירת מצב. Compose הוא framework הצהרתי של ממשק משתמש.
אחד האתגרים בתהליך של יצירה מחדש של כל המסך הוא שזה עלול להיות תהליך יקר מבחינת זמן, כוח מחשוב ושימוש בסוללה. כדי לצמצם את העלות הזו, התכונה 'יצירה' בוחרת בצורה חכמה אילו חלקים בממשק המשתמש צריך לצייר מחדש בכל רגע נתון. יש לכך השלכות מסוימות על האופן שבו מעצבים את רכיבי ממשק המשתמש, כפי שמוסבר במאמר בנושא הרכבה מחדש.
פונקציה הניתנת להגדרה לדוגמה
באמצעות Compose, אתם יכולים ליצור את ממשק המשתמש על ידי הגדרה של קבוצה של פונקציות ניתנות להרכבה שמקבלות נתונים ומפיקות רכיבי ממשק משתמש. דוגמה לכך היא הווידג'ט Greeting, שמקבל String ופולט ווידג'ט Text שמציג הודעת ברכה.
כמה דברים חשובים לגבי הפונקציה הזו:
- הערה: הפונקציה מסומנת בהערה
@Composable. כל הפונקציות שניתנות להרכבה חייבות לכלול את ההערה הזו. ההערה הזו מודיעה לקומפיילר של Compose שהפונקציה הזו נועדה להמיר נתונים לממשק משתמש. - קלט נתונים: הפונקציה מקבלת נתונים. פונקציות קומפוזביליות יכולות לקבל פרמטרים, שמאפשרים ללוגיקה של האפליקציה לתאר את ממשק המשתמש. במקרה הזה, הווידג'ט שלנו מקבל
Stringכדי שהוא יוכל לפנות למשתמש בשם. - תצוגה בממשק המשתמש: הפונקציה מציגה טקסט בממשק המשתמש. היא עושה זאת על ידי הפעלת הפונקציה
Text()composable, שיוצרת בפועל את רכיב ממשק המשתמש של הטקסט. פונקציות קומפוזביליות פולטות היררכיית ממשק משתמש על ידי קריאה לפונקציות קומפוזביליות אחרות. - אין ערך מוחזר: הפונקציה לא מחזירה כלום. פונקציות Compose שיוצרות ממשק משתמש לא צריכות להחזיר ערך כלשהו, כי הן מתארות את מצב המסך הרצוי במקום ליצור רכיבי UI.
מאפיינים: הפונקציה הזו מהירה, אידמפוטנטית ואין לה תופעות לוואי.
- הפונקציה מתנהגת באותו אופן כשקוראים לה כמה פעמים עם אותו ארגומנט, והיא לא משתמשת בערכים אחרים כמו משתנים גלובליים או קריאות ל-
random(). - הפונקציה מתארת את ממשק המשתמש בלי תופעות לוואי, כמו שינוי מאפיינים או משתנים גלובליים.
באופן כללי, כל הפונקציות שניתנות להרכבה חייבות להיכתב עם המאפיינים האלה, מהסיבות שמפורטות בקטע הרכבה מחדש.
- הפונקציה מתנהגת באותו אופן כשקוראים לה כמה פעמים עם אותו ארגומנט, והיא לא משתמשת בערכים אחרים כמו משתנים גלובליים או קריאות ל-
שינוי הפרדיגמה של הצהרה
במערכות רבות של כלים ליצירת ממשקי משתמש מונחי-אובייקטים, מאתחלים את ממשק המשתמש על ידי יצירת מופע של עץ ווידג'טים. לרוב עושים את זה על ידי ניפוח של קובץ פריסת XML. לכל ווידג'ט יש מצב פנימי משלו, והוא חושף שיטות getter ו-setter שמאפשרות ללוגיקה של האפליקציה ליצור אינטראקציה עם הווידג'ט.
בגישה הדקלרטיבית של Compose, הווידג'טים הם יחסית חסרי מצב ולא חושפים פונקציות של setter או getter. למעשה, הווידג'טים לא נחשפים כאובייקטים.
כדי לעדכן את ממשק המשתמש, קוראים לאותה פונקציה שניתנת להרכבה עם ארגומנטים שונים. כך קל יותר לספק מצב לתבניות ארכיטקטוניות כמו ViewModel, כפי שמתואר במדריך לארכיטקטורת אפליקציות. לאחר מכן, הרכיבים הקומפוזביליים אחראים להמיר את המצב הנוכחי של האפליקציה לממשק משתמש בכל פעם שהנתונים שניתנים לצפייה מתעדכנים.
כשהמשתמש מקיים אינטראקציה עם ממשק המשתמש, ממשק המשתמש מעלה אירועים כמו onClick.
האירועים האלה צריכים להודיע ללוגיקה של האפליקציה, ואז היא יכולה לשנות את המצב שלה.
כשמצב משתנה, הפונקציות הקומפוזביליות נקראות שוב עם הנתונים החדשים. כתוצאה מכך, רכיבי ממשק המשתמש מצוירים מחדש – התהליך הזה נקרא קומפוזיציה מחדש.
תוכן דינמי
פונקציות שאפשר להרכיב נכתבות ב-Kotlin ולא ב-XML, ולכן הן יכולות להיות דינמיות כמו כל קוד Kotlin אחר. לדוגמה, נניח שרוצים ליצור ממשק משתמש שמברך רשימה של משתמשים:
@Composable fun Greeting(names: List<String>) { for (name in names) { Text("Hello $name") } }
הפונקציה הזו מקבלת רשימה של שמות ומפיקה ברכה לכל משתמש.
פונקציות הניתנות להגדרה יכולות להיות מורכבות למדי. אפשר להשתמש בהצהרות if כדי להחליט אם רוצים להציג רכיב מסוים בממשק המשתמש. אפשר להשתמש בלולאות. אפשר לקרוא לפונקציות עזר. יש לכם גמישות מלאה בשפה הבסיסית.
העוצמה והגמישות האלה הן אחד היתרונות המרכזיים של Jetpack פיתוח נייטיב.
שינוי הרכב
במודל UI אימפרטיבי, כדי לשנות ווידג'ט, קוראים לפונקציית setter בווידג'ט כדי לשנות את המצב הפנימי שלו. ב-Compose, קוראים שוב לפונקציה הניתנת להרכבה עם נתונים חדשים. הפעולה הזו גורמת לקומפוזיציה מחדש של הפונקציה – הווידג'טים שמוחזרים על ידי הפונקציה מצוירים מחדש, אם צריך, עם נתונים חדשים. ה-framework של Compose יכול להרכיב מחדש בצורה חכמה רק את הרכיבים שהשתנו.
לדוגמה, נניח שיש פונקציה שאפשר להוסיף לה קומפוזיציה, שמציגה לחצן:
@Composable fun ClickCounter(clicks: Int, onClick: () -> Unit) { Button(onClick = onClick) { Text("I've been clicked $clicks times") } }
בכל פעם שלוחצים על הכפתור, המתקשר מעדכן את הערך של clicks.
הפונקציה Compose קוראת שוב לפונקציית ה-lambda עם הפונקציה Text כדי להציג את הערך החדש. התהליך הזה נקרא הרכבה מחדש. פונקציות אחרות שלא תלויות בערך לא מורכבות מחדש.
כמו שציינו, יצירה מחדש של כל עץ ממשק המשתמש יכולה להיות יקרה מבחינת משאבי מחשוב, ולצרוך כוח מחשוב וחיי סוללה. התכונה 'יצירה' פותרת את הבעיה הזו באמצעות שינוי חכם של הטקסט.
קומפוזיציה מחדש היא התהליך של קריאה חוזרת לפונקציות קומפוזביליות כשקלט משתנה. כשכלי העריכה יוצר מחדש תוכן על סמך קלט חדש, הוא קורא רק לפונקציות או לביטויים למדא שעשויים להשתנות, ומדלג על השאר. אם מדלגים על פונקציות או על ביטויי למדה עם פרמטרים שלא השתנו, הפונקציה Compose מבצעת קומפוזיציה מחדש בצורה יעילה.
אל תסתמכו על תופעות לוואי מהרצה של פונקציות קומפוזביליות, כי יכול להיות שדילוג על קומפוזיציה מחדש של פונקציה. אם תעשו את זה, המשתמשים עלולים להיתקל בהתנהגות מוזרה ובלתי צפויה באפליקציה. תופעת לוואי היא כל שינוי שגלוי לשאר האפליקציה. לדוגמה, הפעולות הבאות הן תופעות לוואי מסוכנות:
- כתיבה למאפיין של אובייקט משותף
- עדכון של נתון שאפשר לצפות בו ב-
ViewModel - עדכון העדפות משותפות
יכול להיות שפונקציות שאפשר להרכיב יופעלו מחדש בתדירות גבוהה כמו בכל פריים, למשל כשמבצעים עיבוד של אנימציה. פונקציות שאפשר להרכיב צריכות להיות מהירות כדי למנוע קפיצות במהלך אנימציות. אם אתם צריכים לבצע פעולות יקרות, כמו קריאה מהעדפות משותפות, עשו זאת בקורוטינה ברקע והעבירו את התוצאה של הערך לפונקציה הניתנת להרכבה כפרמטר.
לדוגמה, הקוד הזה יוצר קומפוזיציה לעדכון ערך ב-SharedPreferences. רכיב ה-Composable לא אמור לקרוא או לכתוב מההעדפות המשותפות בעצמו. במקום זאת, הקוד הזה מעביר את פעולות הקריאה והכתיבה אל ViewModel
בקורוטינה ברקע. הלוגיקה של האפליקציה מעבירה את הערך הנוכחי עם קריאה חוזרת (callback) כדי להפעיל עדכון.
@Composable fun SharedPrefsToggle( text: String, value: Boolean, onValueChanged: (Boolean) -> Unit ) { Row { Text(text) Checkbox(checked = value, onCheckedChange = onValueChanged) } }
במסמך הזה מפורטים כמה דברים שחשוב לדעת כשמשתמשים בכתיבה בעזרת AI:
- הרכבה מחדש מדלגת על כמה שיותר פונקציות וביטויי למדא שניתנים להרכבה.
- הסידור מחדש הוא אופטימי ועשוי להתבטל.
- יכול להיות שהרצת פונקציה שאפשר להרכיב תהיה תכופה מאוד, אפילו בכל פריים של אנימציה.
- אפשר להריץ פונקציות הניתנות להגדרה במקביל.
- פונקציות שאפשר להרכיב יכולות לפעול בכל סדר.
בקטעים הבאים מוסבר איך ליצור פונקציות שאפשר להרכיב כדי לתמוך בהרכבה מחדש. בכל מקרה, מומלץ להקפיד שהפונקציות המורכבות יהיו מהירות, אידמפוטנטיות וללא תופעות לוואי.
התאמה מחדש מדלגת על כמה שיותר חלקים
כשחלקים בממשק המשתמש לא תקינים, Compose מנסה לבצע קומפוזיציה מחדש רק של החלקים שצריך לעדכן. כלומר, יכול להיות שהפונקציה תדלג על הרצה חוזרת של רכיב אחד מסוג ButtonComposable בלי להריץ אף אחד מהרכיבים מסוג Composable שנמצאים מעליו או מתחתיו בעץ ממשק המשתמש.
כל פונקציה שניתנת להרכבה וכל פונקציית למבדה עשויות להרכיב את עצמן מחדש. בדוגמה הבאה אפשר לראות איך הרכבה מחדש יכולה לדלג על חלק מהרכיבים כשמעבדים רשימה:
/** * Display a list of names the user can click with a header */ @Composable fun NamePicker( header: String, names: List<String>, onNameClicked: (String) -> Unit ) { Column { // this will recompose when [header] changes, but not when [names] changes Text(header, style = MaterialTheme.typography.bodyLarge) HorizontalDivider() // LazyColumn is the Compose version of a RecyclerView. // The lambda passed to items() is similar to a RecyclerView.ViewHolder. LazyColumn { items(names) { name -> // When an item's [name] updates, the adapter for that item // will recompose. This will not recompose when [header] changes NamePickerItem(name, onNameClicked) } } } } /** * Display a single name the user can click. */ @Composable private fun NamePickerItem(name: String, onClicked: (String) -> Unit) { Text(name, Modifier.clickable(onClick = { onClicked(name) })) }
יכול להיות שכל אחד מההיקפים האלה יהיה הדבר היחיד שיופעל במהלך ההרכבה מחדש.
יכול להיות שהרכיב Compose ידלג אל Column lambda בלי להפעיל אף אחד מהרכיבים ההורים שלו
כשהערך של header משתנה. ובזמן הביצוע של Column, יכול להיות ש-Compose ידלג על הפריטים של LazyColumn אם names לא השתנה.
שוב, כל הפונקציות או הלמדות שניתנות להרכבה לא צריכות להכיל תופעות לוואי. כשצריך לבצע תופעת לוואי, מפעילים אותה מתוך קריאה חוזרת (callback).
ההרכבה מחדש היא אופטימית
קומפוזיציה מחדש מתחילה בכל פעם ש-Compose חושב שהפרמטרים של רכיב קומפוזבילי השתנו. הקומפוזיציה מחדש היא אופטימית,כלומר Compose מצפה לסיים את הקומפוזיציה מחדש לפני שהפרמטרים ישתנו שוב. אם פרמטר משתנה לפני שההרכבה מחדש מסתיימת, יכול להיות ש-Compose יבטל את ההרכבה מחדש ויתחיל אותה מחדש עם הפרמטר החדש.
כשמבטלים את ההרכבה מחדש, Compose מבטל את עץ ממשק המשתמש מההרכבה מחדש. אם יש לכם תופעות לוואי שתלויות בהצגת ממשק המשתמש, תופעת הלוואי תופעל גם אם ההרכבה תבוטל. הדבר עלול לגרום למצב לא עקבי באפליקציה.
כדי לטפל בהרכבה אופטימית, צריך לוודא שכל הפונקציות וה-lambdas שניתנים להרכבה הם אידמפוטנטיים ושהם לא גורמים לתופעות לוואי.
יכול להיות שפונקציות הניתנות להגדרה יפעלו בתדירות גבוהה
במקרים מסוימים, פונקציה קומפוזבילית עשויה לפעול בכל פריים של אנימציה בממשק משתמש. אם הפונקציה מבצעת פעולות שצורכות משאבים רבים, כמו קריאה מאחסון המכשיר, היא עלולה לגרום לבעיות בממשק המשתמש.
לדוגמה, אם הווידג'ט ינסה לקרוא את הגדרות המכשיר, הוא עלול לקרוא את ההגדרות האלה מאות פעמים בשנייה, עם השלכות הרסניות על הביצועים של האפליקציה.
אם פונקציה שאפשר להרכיב ממנה פונקציות אחרות צריכה נתונים, צריך להגדיר פרמטרים לנתונים האלה. אחר כך אפשר להעביר עבודה שדורשת הרבה משאבים לשרשור אחר, מחוץ לקומפוזיציה, ולהעביר את הערך שמתקבל לפונקציה הניתנת להרכבה כפרמטר באמצעות mutableStateOf או LiveData.
פונקציות הניתנות להגדרה יכולות לפעול במקביל
Compose יכול לבצע אופטימיזציה של ההרכבה מחדש על ידי הפעלה של פונקציות שניתנות להרכבה במקביל. כך, Compose יוכל לנצל כמה ליבות ולהריץ פונקציות שאפשר להרכיב שלא מוצגות על המסך בעדיפות נמוכה יותר.
האופטימיזציה הזו תאפשר לפונקציה שניתנת להרכבה לפעול במאגר של שרשורים ברקע.
אם פונקציה קומפוזבילית קוראת לפונקציה ב-ViewModel, יכול להיות ש-Compose יקרא לפונקציה הזו מכמה שרשורים בו-זמנית.
כדי לוודא שהאפליקציה פועלת בצורה תקינה, לכל הפונקציות שניתנות להרכבה לא צריכות להיות תופעות לוואי. במקום זאת, מפעילים תופעות לוואי מקריאות חוזרות (callback) כמו onClick שתמיד מופעלות בשרשור של ממשק המשתמש.
כשמפעילים פונקציה שאפשר להרכיב, ההפעלה עשויה להתרחש בשרשור אחר מהמתקשר. כלומר, צריך להימנע מקוד שמשנה משתנים בביטוי למדא שניתן להרכבה – גם כי הקוד הזה לא בטוח לשימוש בריבוי תהליכים, וגם כי הוא מהווה תופעת לוואי אסורה של ביטוי למדא שניתן להרכבה.
בדוגמה הבאה מוצג קומפוזבל שמציג רשימה ואת מספר הפריטים שבה:
@Composable fun ListComposable(myList: List<String>) { Row(horizontalArrangement = Arrangement.SpaceBetween) { Column { for (item in myList) { Text("Item: $item") } } Text("Count: ${myList.size}") } }
הקוד הזה לא יוצר תופעות לוואי, והוא משנה את רשימת הקלט לממשק משתמש. זהו קוד מצוין להצגת רשימה קטנה. עם זאת, אם הפונקציה כותבת למשתנה מקומי, הקוד הזה לא יהיה בטוח לשימוש בשרשור ולא יהיה נכון:
@Composable fun ListWithBug(myList: List<String>) { var items = 0 Row(horizontalArrangement = Arrangement.SpaceBetween) { Column { for (item in myList) { Card { Text("Item: $item") items++ // Avoid! Side-effect of the column recomposing. } } } Text("Count: $items") } }
בדוגמה הזו, הערך של items משתנה בכל פעם שהתמונה מורכבת מחדש. זה יכול להיות כל פריים באנימציה, או כשמתבצע עדכון ברשימה. בכל מקרה, בממשק המשתמש תוצג ספירה שגויה. לכן, פעולות כתיבה כאלה לא נתמכות ב-Compose. האיסור על פעולות הכתיבה האלה מאפשר למסגרת לשנות את השרשורים כדי להפעיל ביטויי למדה שניתנים להרכבה.
פונקציות הניתנות להגדרה יכולות לפעול בכל סדר
אם תסתכלו על הקוד של פונקציה שאפשר להרכיב, יכול להיות שתניחו שהקוד מורץ לפי הסדר שבו הוא מופיע. אבל אין ערובה לכך שזה נכון. אם פונקציה שאפשר להרכיב מכילה קריאות לפונקציות שאפשר להרכיב אחרות, יכול להיות שהפונקציות האלה יפעלו בכל סדר. ב-Compose יש אפשרות לזהות שחלק מרכיבי ממשק המשתמש חשובים יותר מאחרים, ולצייר אותם קודם.
לדוגמה, נניח שיש לכם קוד כזה כדי לצייר שלושה מסכים בפריסת כרטיסיות:
@Composable fun ButtonRow() { MyFancyNavigation { StartScreen() MiddleScreen() EndScreen() } }
השיחות אל StartScreen, אל MiddleScreen ואל EndScreen עשויות להתבצע בכל סדר. זה אומר שאי אפשר, לדוגמה, ש-StartScreen() יגדיר משתנה גלובלי כלשהו (תופעת לוואי) ו-MiddleScreen() ינצל את השינוי הזה. במקום זאת, כל אחת מהפונקציות האלה צריכה להיות עצמאית.
מידע נוסף
כדי לקבל מידע נוסף על חשיבה ב-Compose ועל פונקציות שאפשר להוסיף, אפשר לעיין במקורות המידע הנוספים הבאים.
סרטונים
מומלץ בשבילכם
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- Kotlin ל-Jetpack פיתוח נייטיב
- מצב ו-Jetpack פיתוח נייטיב
- שכבות ארכיטקטוניות ב-Jetpack פיתוח נייטיב