Jetpack Compose היא ערכת כלים מודרנית להצהרה על ממשק משתמש ב-Android. Compose מפשט את הכתיבה והתחזוקה של ממשק המשתמש של האפליקציה באמצעות API הצהרתי שמאפשר לעבד את ממשק המשתמש של האפליקציה בלי לשנות באופן הכרחי את התצוגות של ממשק המשתמש. המונחים האלה דורשים הסבר, אבל ההשלכות שלהם חשובות לעיצוב האפליקציה.
פרדיגמת התכנות הדקלרטיבי
בעבר, היררכיית תצוגות ב-Android הייתה מיוצגת כעץ של ווידג'טים בממשק המשתמש. כשהמצב של האפליקציה משתנה בגלל דברים כמו אינטראקציות של משתמשים, צריך לעדכן את היררכיית ממשק המשתמש כדי להציג את הנתונים הנוכחיים.
הדרך הנפוצה ביותר לעדכן את ממשק המשתמש היא לעבור על העץ באמצעות פונקציות כמו findViewById(), ולשנות צמתים על ידי קריאה לשיטות כמו button.setText(String), container.addChild(View) או img.setImageBitmap(Bitmap). השיטות האלה משנות את המצב הפנימי של הווידג'ט.
שינוי ידני של תצוגות מגדיל את הסיכוי לשגיאות. אם נתון מסוים מוצג בכמה מקומות, יכול להיות שתשכחו לעדכן את אחת מהתצוגות שבהן הוא מופיע. זה גם יכול להוביל למצבים לא חוקיים, כששני עדכונים מתנגשים באופן לא צפוי. לדוגמה, יכול להיות שעדכון ינסה להגדיר ערך של צומת שהוסר מהממשק. באופן כללי, ככל שיש יותר תצוגות שצריך לעדכן, כך תחזוקת התוכנה מורכבת יותר.
בשנים האחרונות, כל התעשייה התחילה לעבור למודל דקלרטיבי של ממשק משתמש, שמפשט את הבנייה והעדכון של ממשקי משתמש.
השיטה הזו פועלת על ידי יצירה מחדש של כל המסך מאפס, ואז החלת השינויים הנדרשים בלבד. הגישה הזו מונעת את המורכבות של עדכון ידני של היררכיית תצוגות עם שמירת מצב. Compose הוא מסגרת דקלרטיבית של ממשק משתמש.
אחד האתגרים ביצירה מחדש של המסך כולו הוא שזה עלול להיות יקר מבחינת זמן, כוח מחשוב ושימוש בסוללה. כדי לצמצם את העלות הזו, Compose בוחר באופן חכם אילו חלקים בממשק המשתמש צריך לשרטט מחדש בכל זמן נתון. יש לכך השלכות על האופן שבו אתם מעצבים את רכיבי ממשק המשתמש, כפי שמוסבר בקטע יצירה מחדש של קומפוזיציה.
דוגמה לפונקציה קומפוזבילית
באמצעות Compose, אתם יכולים ליצור את ממשק המשתמש על ידי הגדרת קבוצה של פונקציות קומפוזביליות שמקבלות נתונים ופולטות רכיבים בממשק המשתמש. דוגמה לכך היא ווידג'ט Greeting, שמקבל String ופולט ווידג'ט Text שמציג הודעת ברכה.
כמה דברים שחשוב לדעת על הפונקציה הזו:
- הערה: הפונקציה מסומנת בהערה
@Composable. כל הפונקציות הקומפוזביליות חייבות לכלול את ההערה הזו. ההערה הזו מודיעה למהדר של Compose שהפונקציה הזו מיועדת להמיר נתונים לממשק משתמש. - קלט נתונים: הפונקציה מקבלת נתונים. פונקציות קומפוזביליות יכולות לקבל פרמטרים, שמאפשרים ללוגיקה של האפליקציה לתאר את ממשק המשתמש. במקרה הזה, הווידג'ט שלנו מקבל
Stringכדי שיוכל לברך את המשתמש בשמו. - תצוגה בממשק המשתמש: הפונקציה מציגה טקסט בממשק המשתמש. כדי לעשות את זה, היא שולחת קריאה לפונקציה הקומפוזבילית
Text(), שיוצרת בפועל את רכיב הטקסט בממשק המשתמש. פונקציות קומפוזביליות פולטות היררכיית ממשק משתמש על ידי קריאה לפונקציות קומפוזביליות אחרות. - אין ערך מוחזר: הפונקציה לא מחזירה שום דבר. פונקציות Compose שיוצרות ממשק משתמש לא צריכות להחזיר שום דבר, כי הן מתארות את מצב מסך היעד במקום ליצור ווידג'טים של ממשק משתמש.
מאפיינים: הפונקציה הזו מהירה, אידמפוטנטית וללא תופעות לוואי.
- הפונקציה פועלת באותו אופן כשקוראים לה כמה פעמים עם אותו ארגומנט, והיא לא משתמשת בערכים אחרים כמו משתנים גלובליים או קריאות ל-
random(). - הפונקציה מתארת את ממשק המשתמש בלי תופעות לוואי, כמו שינוי מאפיינים או משתנים גלובליים.
באופן כללי, כל הפונקציות הקומפוזביליות חייבות להיכתב עם המאפיינים האלה, מהסיבות שמפורטות בקטע יצירה מחדש (recomposition).
- הפונקציה פועלת באותו אופן כשקוראים לה כמה פעמים עם אותו ארגומנט, והיא לא משתמשת בערכים אחרים כמו משתנים גלובליים או קריאות ל-
שינוי הפרדיגמה של גישת החיווי
בממשקי משתמש רבים שמונחי עצמים, מאתחלים את ממשק המשתמש על ידי יצירת עץ של ווידג'טים. לרוב עושים את זה על ידי ניפוח של קובץ פריסה ב-XML. כל ווידג'ט שומר על המצב הפנימי שלו, וחושף שיטות getter ו-setter שמאפשרות ללוגיקה של האפליקציה לקיים אינטראקציה עם הווידג'ט.
בגישה ההצהרתית של Compose, הווידג'טים הם יחסית חסרי מצב (stateless) ולא חושפים פונקציות setter או getter. למעשה, ווידג'טים לא מוצגים כאובייקטים.
כדי לעדכן את ממשק המשתמש, צריך להפעיל את אותה פונקציה קומפוזבילית עם ארגומנטים שונים. כך קל יותר לספק מצב לתבניות ארכיטקטורה כמו ViewModel, כפי שמתואר במדריך לארכיטקטורת אפליקציות. לאחר מכן, הרכיבים הקומפוזביליים אחראים להפוך את מצב האפליקציה הנוכחי לממשק משתמש בכל פעם שהנתונים הגלויים מתעדכנים.
כשהמשתמש מבצע אינטראקציה עם ממשק המשתמש, ממשק המשתמש מעלה אירועים כמו onClick.
האירועים האלה אמורים להודיע ללוגיקה של האפליקציה, שיכולה לשנות את המצב של האפליקציה.
כשהמצב משתנה, הפונקציות הקומפוזביליות נקראות שוב עם הנתונים החדשים. זה גורם לרכיבים בממשק המשתמש להשתנות – התהליך הזה נקרא יצירה מחדש.
תוכן דינמי
מכיוון שפונקציות קומפוזביליות נכתבות ב-Kotlin במקום ב-XML, הן יכולות להיות דינמיות כמו כל קוד Kotlin אחר. לדוגמה, נניח שאתם רוצים ליצור ממשק משתמש שמברך רשימה של משתמשים:
@Composable fun Greeting(names: List<String>) { for (name in names) { Text("Hello $name") } }
הפונקציה הזו מקבלת רשימה של שמות ויוצרת ברכה לכל משתמש.
פונקציות קומפוזביליות יכולות להיות מתוחכמות מאוד. אתם יכולים להשתמש בהצהרות if כדי להחליט אם להציג רכיב בממשק המשתמש. אפשר להשתמש בלולאות. אפשר לקרוא לפונקציות עזר. יש לכם את כל הגמישות של השפה הבסיסית.
העוצמה והגמישות האלה הן אחד מהיתרונות העיקריים של Jetpack Compose.
רה-קומפוזיציה
במודל של ממשק משתמש אימפרטיבי, כדי לשנות ווידג'ט, צריך להפעיל פונקציית 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 כדי להציג את הערך החדש. התהליך הזה נקרא יצירה מחדש (recomposition). פונקציות אחרות שלא תלויות בערך לא נוצרות מחדש.
כמו שציינו, הרכבה מחדש של כל עץ ממשק המשתמש יכולה להיות יקרה מבחינת משאבי המחשוב, ולצרוך הרבה כוח עיבוד וסוללה. Compose פותר את הבעיה הזו באמצעות הרכבה מחדש חכמה.
קומפוזיציה מחדש היא התהליך של קריאה חוזרת לפונקציות הקומפוזביליות כשהקלט משתנה. כשהרכבה מחדש מתבצעת ב-Compose על סמך נתוני קלט חדשים, המערכת קוראת רק לפונקציות או לביטויי הלמבדה שעשויים להשתנות, ומדלגת על השאר. כך, Compose מדלג על פונקציות או על פונקציות למבדה עם פרמטרים שלא השתנו, ומבצע קומפוזיציה מחדש בצורה יעילה.
אל תסתמכו על תופעות לוואי מהרצת פונקציות קומפוזביליות, כי יכול להיות שהמערכת תדלג על הרכבה מחדש של פונקציה. אם תעשו זאת, המשתמשים עלולים לחוות התנהגות מוזרה ובלתי צפויה באפליקציה. תופעת לוואי היא כל שינוי שגלוי לשאר חלקי האפליקציה. לדוגמה, הפעולות הבאות הן כולן תופעות לוואי מסוכנות:
- כתיבה למאפיין של אובייקט משותף
- עדכון של אובייקט Observable ב-
ViewModel - עדכון ההעדפות המשותפות
יכול להיות שהפונקציות הקומפוזביליות יופעלו מחדש בכל פריים, למשל כשמתבצע רינדור של אנימציה. הפונקציות הקומפוזביליות צריכות להיות מהירות כדי למנוע תנועות קופצניות במהלך האנימציות. אם אתם צריכים לבצע פעולות יקרות, כמו קריאה מהעדפות משותפות, בצעו אותן בשגרת המשך (coroutine) ברקע והעבירו את תוצאת הערך לפונקציה קומפוזבילית כפרמטר.
לדוגמה, הקוד הזה יוצר רכיב קומפוזבילי לעדכון ערך ב-SharedPreferences. הפונקציה הקומפוזבילית לא אמורה לקרוא או לכתוב מהעדפות משותפות בעצמה. במקום זאת, הקוד הזה מעביר את הקריאה והכתיבה אל ViewModel
ב-coroutine ברקע. הלוגיקה של האפליקציה מעבירה את הערך הנוכחי באמצעות קריאה חוזרת (callback) כדי להפעיל עדכון.
@Composable fun SharedPrefsToggle( text: String, value: Boolean, onValueChanged: (Boolean) -> Unit ) { Row { Text(text) Checkbox(checked = value, onCheckedChange = onValueChanged) } }
במסמך הזה מפורטים כמה דברים שחשוב לדעת כשמשתמשים ב-Compose:
- בתהליך ההרכבה מחדש, המערכת מדלגת על כמה שיותר פונקציות קומפוזביליות ופונקציות LAMBDA.
- ההרכבה מחדש היא אופטימית ויכול להיות שהיא תבוטל.
- יכול להיות שפונקציה קומפוזבילית תופעל לעיתים קרובות, אפילו בכל פריים של אנימציה.
- אפשר להריץ פונקציות קומפוזביליות במקביל.
- אפשר להריץ פונקציות קומפוזביליות בכל סדר.
בקטעים הבאים נסביר איך ליצור פונקציות קומפוזביליות שתומכות ביצירה מחדש. בכל מקרה, השיטה המומלצת היא לשמור על הפונקציות הקומפוזביליות מהירות, אידמפוטנטיות וללא תופעות לוואי.
הרכבה מחדש מדלגת על כמה שיותר
כשחלקים מממשק המשתמש לא תקינים, Compose מנסה ליצור מחדש רק את החלקים שצריך לעדכן. כלומר, יכול להיות שהיא תדלג על הרצה מחדש של פונקציית composable אחת של Button בלי להריץ אף אחת מהפונקציות 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 בלי להריץ אף אחד מההורים שלו כשהערך של header ישתנה. וכשמפעילים את Column, יכול להיות ש-Compose ידלג על הפריטים של LazyColumn אם names לא השתנה.
שוב, כל הפונקציות הקומפוזביליות או פונקציות הלמדה צריכות להיות ללא תופעות לוואי. כשצריך לבצע פעולה משנית, מפעילים אותה מ-callback.
הרכבה מחדש היא אופטימית
הקומפוזיציה מחדש מתחילה בכל פעם ש-Compose מזהה שהפרמטרים של רכיב קומפוזבילי השתנו. ההרכבה מחדש היא אופטימית,כלומר Compose מצפה לסיים את ההרכבה מחדש לפני שהפרמטרים ישתנו שוב. אם פרמטר משתנה לפני שההרכבה מחדש מסתיימת, יכול להיות ש-Compose יבטל את ההרכבה מחדש ויפעיל אותה מחדש עם הפרמטר החדש.
כשההרכבה מחדש מבוטלת, Compose מבטל את עץ ממשק המשתמש מההרכבה מחדש. אם יש לכם תופעות לוואי שתלויות בהצגת ממשק המשתמש, תופעת הלוואי תופעל גם אם הקומפוזיציה תבוטל. זה יכול לגרום למצב לא עקבי באפליקציה.
כדי לטפל בהרכבה מחדש אופטימית, צריך לוודא שכל הפונקציות הקומפוזביליות וביטויי הלמבדה הם אידמפוטנטיים וללא תופעות לוואי.
פונקציות קומפוזביליות עשויות לפעול בתדירות גבוהה למדי
במקרים מסוימים, פונקציה קומפוזבילית עשויה לפעול בכל פריים של אנימציה בממשק המשתמש. אם הפונקציה מבצעת פעולות שצורכות משאבים רבים, כמו קריאה מהאחסון במכשיר, היא עלולה לגרום לבעיות בביצועים של ממשק המשתמש.
לדוגמה, אם הווידג'ט ינסה לקרוא את הגדרות המכשיר, הוא עלול לקרוא אותן מאות פעמים בשנייה, מה שיפגע מאוד בביצועי האפליקציה.
אם פונקציה קומפוזבילית צריכה נתונים, מגדירים פרמטרים לנתונים האלה. אחר כך אפשר להעביר את העבודה שדורשת הרבה משאבים לשרשור אחר, מחוץ לקומפוזיציה, ולהעביר את הערך שמתקבל לפונקציה הקומפוזבילית כפרמטר באמצעות mutableStateOf או LiveData.
פונקציות קומפוזביליות יכולות לפעול במקביל
Compose יכול לבצע אופטימיזציה של הרכבה מחדש על ידי הפעלת פונקציות קומפוזביליות במקביל. כך Compose יכול לנצל כמה ליבות, ולהריץ פונקציות קומפוזביליות שלא מופיעות במסך בעדיפות נמוכה יותר.
האופטימיזציה הזו אומרת שפונקציה קומפוזבילית יכולה לפעול בתוך מאגר של שרשורים ברקע.
אם פונקציה קומפוזבילית קוראת לפונקציה ב-ViewModel, יכול להיות ש-Compose תקרא לפונקציה הזו מכמה שרשורים בו-זמנית.
כדי לוודא שהאפליקציה פועלת בצורה תקינה, לפונקציות הקומפוזביליות לא אמורות להיות תופעות לוואי. במקום זאת, צריך להפעיל את ההשפעות המשניות מהקריאות החוזרות (callback) כמו onClick, שתמיד פועלות בשרשור ה-UI.
כשפונקציה קומפוזבילית מופעלת, יכול להיות שההפעלה תתבצע בשרשור שונה מזה של הפונקציה שקוראת לה. כלומר, צריך להימנע מקוד שמשנה משתנים ב-composable lambda – גם כי קוד כזה הוא לא thread-safe, וגם כי זו תופעת לוואי אסורה של ה-composable lambda.
הנה דוגמה לפונקציה קומפוזבילית שמציגה רשימה ואת מספר הפריטים שבה:
@Composable fun ListComposable(myList: List<String>) { Row(horizontalArrangement = Arrangement.SpaceBetween) { Column { for (item in myList) { Text("Item: $item") } } Text("Count: ${myList.size}") } }
הקוד הזה לא גורם לתופעות לוואי, והוא הופך את רשימת הקלט לממשק משתמש. זה קוד מצוין להצגת רשימה קטנה. אבל אם הפונקציה כותבת למשתנה מקומי, הקוד הזה לא יהיה thread-safe או נכון:
@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. האיסור על כתיבה כזו מאפשר ל-framework לשנות שרשורים כדי להפעיל פונקציות למבדה קומפוזביליות.
אפשר להריץ פונקציות קומפוזביליות בכל סדר
אם תסתכלו על הקוד של פונקציה קומפוזבילית, יכול להיות שתניחו שהקוד פועל לפי הסדר שבו הוא מופיע. אבל זה לא בהכרח נכון. אם פונקציה קומפוזבילית מכילה קריאות לפונקציות קומפוזביליות אחרות, יכול להיות שהפונקציות האלה יפעלו בכל סדר. ב-Compose יש אפשרות לזהות שרכיבים מסוימים בממשק המשתמש הם בעדיפות גבוהה יותר מאחרים, ולשרטט אותם קודם.
לדוגמה, נניח שיש לכם קוד כזה כדי לצייר שלושה מסכים בפריסת כרטיסיות:
@Composable fun ButtonRow() { MyFancyNavigation { StartScreen() MiddleScreen() EndScreen() } }
הקריאות לפונקציות StartScreen, MiddleScreen ו-EndScreen יכולות להתבצע בכל סדר. זה אומר שאי אפשר, לדוגמה, להגדיר משתנה גלובלי (תופעת לוואי) ב-StartScreen() ולנצל את השינוי הזה ב-MiddleScreen(). במקום זאת, כל אחת מהפונקציות האלה צריכה להיות עצמאית.
מידע נוסף
כדי לקבל מידע נוסף על אופן החשיבה ב-Compose ועל פונקציות קומפוזביליות, אפשר לעיין במקורות המידע הנוספים הבאים.
סרטונים
מומלץ בשבילכם
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- Kotlin ל-Jetpack Compose
- מצב ו-Jetpack פיתוח נייטיב
- שכבות ארכיטקטורה של Jetpack Compose