Jetpack פיתוח נייטיב מבוסס על Kotlin. במקרים מסוימים, ב-Kotlin יש ביטויים מיוחדים שמקלים על כתיבת קוד טוב ב-Compose. אם אתם חושבים בשפת תכנות אחרת ומתורגמים את השפה הזו בראש ל-Kotlin, סביר להניח שתפספסו חלק מהיתרונות של Compose, ויכול להיות שתתקשו להבין קוד Kotlin שנכתב באופן שגור. כדי להימנע מהמלכודות האלה, מומלץ להכיר טוב יותר את הסגנון של Kotlin.
ארגומנטים שמוגדרים כברירת מחדל
כשכותבים פונקציה ב-Kotlin, אפשר לציין ערכים שמוגדרים כברירת מחדל לארגומנטים של הפונקציה, שייעשה בהם שימוש אם מבצע הקריאה לא מעביר את הערכים האלה באופן מפורש. התכונה הזו מפחיתה את הצורך בפונקציות עם עומס יתר.
לדוגמה, נניח שרוצים לכתוב פונקציה שמשרטטת ריבוע. יכול להיות לפונקציה הזו פרמטר נדרש אחד, sideLength, שמציין את האורך של כל צלע. יכולים להיות לה כמה פרמטרים אופציונליים, כמו thickness, edgeColor וכו'. אם מבצע הקריאה לא מציין את הפרמטרים האלה, הפונקציה משתמשת בערכי ברירת המחדל. בשפות אחרות, יכול להיות שתצטרכו לכתוב כמה פונקציות:
// We don't need to do this in Kotlin! void drawSquare(int sideLength) { } void drawSquare(int sideLength, int thickness) { } void drawSquare(int sideLength, int thickness, Color edgeColor) { }
ב-Kotlin, אפשר לכתוב פונקציה אחת ולציין את ערכי ברירת המחדל של הארגומנטים:
fun drawSquare( sideLength: Int, thickness: Int = 2, edgeColor: Color = Color.Black ) { }
בנוסף לחיסכון בצורך לכתוב כמה פונקציות מיותרות, התכונה הזו מאפשרת לקרוא את הקוד בצורה ברורה יותר. אם מבצע הקריאה לא מציין ערך לארגומנט, המשמעות היא שהוא מוכן להשתמש בערך ברירת המחדל. בנוסף, הפרמטרים עם השמות עוזרים לכם להבין מה קורה בקלות רבה יותר. אם תעיינו בקוד ותראו קריאה לפונקציה כזו, יכול להיות שלא תדעו מה המשמעות של הפרמטרים בלי לבדוק את הקוד של drawSquare()
:
drawSquare(30, 5, Color.Red);
לעומת זאת, הקוד הזה מתעד את עצמו:
drawSquare(sideLength = 30, thickness = 5, edgeColor = Color.Red)
ברוב הספריות של Compose נעשה שימוש בארגומנטים שמוגדרים כברירת מחדל, וזו שיטה מומלצת גם לפונקציות הניתנות לקיבוץ שאתם כותבים. כך תוכלו להתאים אישית את הרכיבים הניתנים לשילוב, אבל עדיין להפעיל בקלות את התנהגות ברירת המחדל. לדוגמה, אפשר ליצור רכיב טקסט פשוט כך:
Text(text = "Hello, Android!")
לקוד הזה יש את אותו אפקט כמו לקוד הבא, ארוך יותר, שבו מוגדר באופן מפורש מספר גדול יותר של הפרמטרים של Text
:
Text( text = "Hello, Android!", color = Color.Unspecified, fontSize = TextUnit.Unspecified, letterSpacing = TextUnit.Unspecified, overflow = TextOverflow.Clip )
קטע הקוד הראשון לא רק פשוט יותר וקל יותר לקריאה, אלא גם מתעד את עצמו. כשמציינים רק את הפרמטר text
, מתועדת העובדה שרוצים להשתמש בערכי ברירת המחדל לכל הפרמטרים האחרים. לעומת זאת, הקטע השני מרמז שאתם רוצים להגדיר באופן מפורש את הערכים של הפרמטרים האחרים האלה, למרות שהערכים שהגדרתם הם ערכי ברירת המחדל של הפונקציה.
פונקציות מסדר גבוה יותר וביטויי lambda
ב-Kotlin יש תמיכה בפונקציות ברמה גבוהה יותר – פונקציות שמקבלות פונקציות אחרות כפרמטרים. Compose מבוסס על הגישה הזו. לדוגמה, הפונקציה הניתנת לקישור Button
מספקת פרמטר lambda onClick
. הערך של הפרמטר הזה הוא פונקציה, שהלחצן קורא לה כשהמשתמש לוחץ עליו:
Button( // ... onClick = myClickFunction ) // ...
פונקציות מסדר גבוה יותר מתאימות באופן טבעי לביטויי למדא, ביטויים שמתקבל מהם פונקציה. אם אתם צריכים את הפונקציה רק פעם אחת, אתם לא צריכים להגדיר אותה במקום אחר כדי להעביר אותה לפונקציה ברמה גבוהה יותר. במקום זאת, אפשר פשוט להגדיר את הפונקציה במקום באמצעות ביטוי lambda. בדוגמה הקודמת, ההנחה היא ש-myClickFunction()
מוגדר במקום אחר. אבל אם משתמשים בפונקציה הזו רק כאן, קל יותר פשוט להגדיר את הפונקציה בתוך שורת הקוד באמצעות ביטוי lambda:
Button( // ... onClick = { // do something // do something else } ) { /* ... */ }
פונקציות lambda בסוף
ב-Kotlin יש תחביר מיוחד לקריאה לפונקציות מסדר גבוה יותר שהפרמטר האחרון שלהן הוא פונקציית lambda. אם רוצים להעביר ביטוי lambda בתור הפרמטר הזה, אפשר להשתמש בתחביר של lambda בסוגריים נגררים. במקום להציב את ביטוי הלמה בתוך הסוגריים, צריך להציב אותו אחריהם. זוהי סיטואציה נפוצה ב-Compose, לכן חשוב להכיר את המראה של הקוד.
לדוגמה, הפרמטר האחרון לכל הפריסות, כמו הפונקציה הניתנת ליצירה Column()
, הוא content
, פונקציה שמפיקה את רכיבי ממשק המשתמש הצאצאים. נניח שרוצים ליצור עמודה שמכילה שלושה רכיבי טקסט, וצריכים להחיל פורמט מסוים. הקוד הזה יפעל, אבל הוא מאוד מסורבל:
Column( modifier = Modifier.padding(16.dp), content = { Text("Some text") Text("Some more text") Text("Last text") } )
מכיוון שהפרמטר content
הוא הפרמטר האחרון בחתימה של הפונקציה, ואנחנו מעבירים את הערך שלו כביטוי lambda, אפשר להוציא אותו מהסוגריים:
Column(modifier = Modifier.padding(16.dp)) { Text("Some text") Text("Some more text") Text("Last text") }
לשתי הדוגמאות יש בדיוק את אותה משמעות. סוגרי הסוגריים מגדירים את ביטוי הלמבדה שמוענק לפרמטר content
.
למעשה, אם הפרמטר היחיד שאתם מעבירים הוא פונקציית הלוגריתם ההפוך שבסוף – כלומר, אם הפרמטר האחרון הוא פונקציית לוגריתם הפוך ואתם לא מעבירים פרמטרים אחרים – תוכלו להשמיט את הסוגריים הסוגריים לגמרי. לדוגמה, נניח שלא צריך להעביר שינוי ל-Column
. אפשר לכתוב את הקוד כך:
Column { Text("Some text") Text("Some more text") Text("Last text") }
התחביר הזה נפוץ למדי ב-Compose, במיוחד לגבי רכיבי פריסה כמו Column
. הפרמטר האחרון הוא ביטוי lambda שמגדיר את הצאצאים של הרכיב, והצאצאים האלה מצוינים בסוגריים מסולסלים אחרי קריאת הפונקציה.
היקפי הרשאות ונמענים
שיטות ומאפיינים מסוימים זמינים רק בהיקף מסוים. ההיקף המוגבל מאפשר לכם להציע פונקציונליות כשהיא נדרשת, ולהימנע משימוש בטעות בפונקציונליות הזו כשהיא לא מתאימה.
נבחן דוגמה לשימוש ב-Compose. כשאתם קוראים ל-layout composable של Row
, פונקציית הלוגריתם של התוכן מופעלת באופן אוטומטי בתוך RowScope
.
כך אפשר לחשוף ב-Row
פונקציונליות שתקפה רק ב-Row
.
בדוגמה הבאה מוצג איך Row
חשף ערך ספציפי לשורה של המשתנה המשנה align
:
Row { Text( text = "Hello world", // This Text is inside a RowScope so it has access to // Alignment.CenterVertically but not to // Alignment.CenterHorizontally, which would be available // in a ColumnScope. modifier = Modifier.align(Alignment.CenterVertically) ) }
חלק מממשקי ה-API מקבלים פונקציות lambda שנקראות בהיקף הנמען. ל-lambdas האלה יש גישה למאפיינים ולפונקציות שמוגדרים במקום אחר, על סמך הצהרת הפרמטר:
Box( modifier = Modifier.drawBehind { // This method accepts a lambda of type DrawScope.() -> Unit // therefore in this lambda we can access properties and functions // available from DrawScope, such as the `drawRectangle` function. drawRect( /*...*/ /* ... ) } )
מידע נוסף זמין במאמר פונקציות לינטרליות עם מקבל במסמכי התיעוד של Kotlin.
נכסים שהוקצו
ב-Kotlin יש תמיכה במאפיינים שהועברו.
הנכסים האלה נקראים כאילו הם שדות, אבל הערך שלהם נקבע באופן דינמי על ידי הערכת ביטוי. אפשר לזהות את המאפיינים האלה לפי השימוש שלהם בתחביר by
:
class DelegatingClass { var name: String by nameGetterFunction() // ... }
קוד אחר יכול לגשת לנכס באמצעות קוד כזה:
val myDC = DelegatingClass() println("The name property is: " + myDC.name)
כשהפעולה println()
מתבצעת, הפונקציה nameGetterFunction()
נקראת כדי להחזיר את הערך של המחרוזת.
המאפיינים המוענקים במתנה שימושיים במיוחד כשעובדים עם מאפיינים שמגובים במצב:
var showDialog by remember { mutableStateOf(false) } // Updating the var automatically triggers a state change showDialog = true
ביטול המבנה של כיתות נתונים
אם מגדירים class של נתונים, אפשר לגשת בקלות לנתונים באמצעות הצהרה של ניתוח מבנה. לדוגמה, נניח שמגדירים את הכיתה Person
:
data class Person(val name: String, val age: Int)
אם יש לכם אובייקט מהסוג הזה, תוכלו לגשת לערכים שלו באמצעות קוד כמו זה:
val mary = Person(name = "Mary", age = 35) // ... val (name, age) = mary
לרוב, תוכלו לראות קוד כזה בפונקציות של Compose:
Row { val (image, title, subtitle) = createRefs() // The `createRefs` function returns a data object; // the first three components are extracted into the // image, title, and subtitle variables. // ... }
יש לכיתות הנתונים הרבה פונקציונליות שימושית נוספת. לדוגמה, כשמגדירים סוג נתונים, המהדרר מגדיר באופן אוטומטי פונקציות שימושיות כמו equals()
ו-copy()
. מידע נוסף זמין במאמר בנושא כיתות נתונים.
אובייקטים מסוג Singleton
ב-Kotlin קל להצהיר על מונותים, כלומר על כיתות שיש להן תמיד מופע אחד בלבד. המודולים האלה מוצהרים באמצעות מילת המפתח object
.
לעיתים קרובות נעשה שימוש באובייקטים כאלה ב-Compose. לדוגמה, MaterialTheme
מוגדר כאובייקט יחיד (singleton). המאפיינים MaterialTheme.colors
, shapes
ו-typography
מכילים את הערכים של העיצוב הנוכחי.
בוני DSL ו-DSL בטוחים לסוגים
ב-Kotlin אפשר ליצור שפות ייעודיות לדומיין (DSL) באמצעות בוני קוד בטוחים לסוגים. שפות DSL מאפשרות ליצור מבני נתונים מורכבים והיררכיים בצורה קריאה וקלה יותר לתחזוקה.
ב-Jetpack Compose נעשה שימוש ב-DSLs ל-API מסוימים, כמו LazyRow
ו-LazyColumn
.
@Composable fun MessageList(messages: List<Message>) { LazyColumn { // Add a single item as a header item { Text("Message List") } // Add list of messages items(messages) { message -> Message(message) } } }
ב-Kotlin אפשר להבטיח בוני טיפים בטוחים באמצעות ליטרלים של פונקציות עם מקלט.
לדוגמה, אם ניקח את ה-composable Canvas
, הוא מקבל כפרמטר פונקציה עם DrawScope
בתור הנמען, onDraw: DrawScope.() -> Unit
, שמאפשרת לבלוק הקוד לבצע קריאה לפונקציות החברים שמוגדרות ב-DrawScope
.
Canvas(Modifier.size(120.dp)) { // Draw grey background, drawRect function is provided by the receiver drawRect(color = Color.Gray) // Inset content by 10 pixels on the left/right sides // and 12 by the top/bottom inset(10.0f, 12.0f) { val quadrantSize = size / 2.0f // Draw a rectangle within the inset bounds drawRect( size = quadrantSize, color = Color.Red ) rotate(45.0f) { drawRect(size = quadrantSize, color = Color.Blue) } } }
מידע נוסף על בוני DSL ו-DSL ללא שגיאות בטיחות סוג זמין במסמכי התיעוד של Kotlin.
שגרות המשך (coroutines) ב-Kotlin
קורוטינים מספקים תמיכה בתכנות אסינכרוני ברמת השפה ב-Kotlin. פונקציות קורוטין יכולות להשהות את הביצוע בלי לחסום את השרשור. ממשק משתמש תגובתי הוא מטבעו אסינכרוני, ו-Jetpack Compose פותר את הבעיה הזו על ידי שימוש ב-coroutines ברמת ה-API במקום להשתמש ב-callbacks.
ב-Jetpack Compose יש ממשקי API שמאפשרים להשתמש ב-coroutines בצורה בטוחה בשכבת ממשק המשתמש.
הפונקציה rememberCoroutineScope
מחזירה CoroutineScope
שבעזרתו אפשר ליצור פונקציות רפיטיביות בטיפולי אירועים ולקרוא לממשקי API להשהיה של Compose. בדוגמה הבאה נעשה שימוש ב-animateScrollTo
API של ScrollState
.
// Create a CoroutineScope that follows this composable's lifecycle val composableScope = rememberCoroutineScope() Button( // ... onClick = { // Create a new coroutine that scrolls to the top of the list // and call the ViewModel to load data composableScope.launch { scrollState.animateScrollTo(0) // This is a suspend function viewModel.loadData() } } ) { /* ... */ }
כברירת מחדל, פונקציות קורוטין מריצות את בלוק הקוד באופן רציף. כשקורוטין פעיל קורא לפונקציית השהיה, הוא משהה את הביצוע שלו עד שפונקציית ההשהיה מחזירה תשובה. הדבר נכון גם אם פונקציית ההשעיה מעבירה את הביצועים ל-CoroutineDispatcher
אחר. בדוגמה הקודמת, הפונקציה loadData
לא תבוצע עד שהפונקציה להשהיה animateScrollTo
תחזיר תשובה.
כדי להריץ קוד בו-זמנית, צריך ליצור קורוטינים חדשים. בדוגמה שלמעלה, כדי לבצע במקביל את הגלילה לחלק העליון של המסך ואת טעינת הנתונים מ-viewModel
, נדרשים שני קורוטינים.
// Create a CoroutineScope that follows this composable's lifecycle val composableScope = rememberCoroutineScope() Button( // ... onClick = { // Scroll to the top and load data in parallel by creating a new // coroutine per independent work to do composableScope.launch { scrollState.animateScrollTo(0) } composableScope.launch { viewModel.loadData() } } ) { /* ... */ }
פונקציות קורוטין מאפשרות לשלב בקלות ממשקי API אסינכררוניים. בדוגמה הבאה משלבים את המשתנה המשנה pointerInput
עם ממשקי ה-API של האנימציה כדי ליצור אנימציה של מיקום רכיב כשהמשתמש מקשקש על המסך.
@Composable fun MoveBoxWhereTapped() { // Creates an `Animatable` to animate Offset and `remember` it. val animatedOffset = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) } Box( // The pointerInput modifier takes a suspend block of code Modifier .fillMaxSize() .pointerInput(Unit) { // Create a new CoroutineScope to be able to create new // coroutines inside a suspend function coroutineScope { while (true) { // Wait for the user to tap on the screen val offset = awaitPointerEventScope { awaitFirstDown().position } // Launch a new coroutine to asynchronously animate to // where the user tapped on the screen launch { // Animate to the pressed position animatedOffset.animateTo(offset) } } } } ) { Text("Tap anywhere", Modifier.align(Alignment.Center)) Box( Modifier .offset { // Use the animated offset as the offset of this Box IntOffset( animatedOffset.value.x.roundToInt(), animatedOffset.value.y.roundToInt() ) } .size(40.dp) .background(Color(0xff3c1361), CircleShape) ) }
מידע נוסף על קורוטינים זמין במדריך קורוטינים ב-Kotlin ב-Android.
מומלץ עבורך
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- רכיבים ופלטפורמות של Material Design
- תופעות לוואי ב-Compose
- יסודות של יצירת פריסה