Kotlin ל-Jetpack פיתוח נייטיב

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

ארגומנטים של ברירת מחדל

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

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

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

Text(text = "Hello, Android!")

לקוד הזה יש אותה השפעה כמו לקוד הבא, אבל מפורט הרבה יותר, שבו יותר תוכן Text הפרמטרים מוגדרים במפורש:

Text(
    text = "Hello, Android!",
    color = Color.Unspecified,
    fontSize = TextUnit.Unspecified,
    letterSpacing = TextUnit.Unspecified,
    overflow = TextOverflow.Clip
)

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

פונקציות מסדר גבוה יותר וביטויי למבדה

Kotlin תומך בסדר גבוה יותר פונקציות, מקבלים פונקציות אחרות כפרמטרים. פיתוח הכתיבה מתבסס על הגישה הזו. עבור לדוגמה, Button הפונקציה הקומפוזבילית מספקת פרמטר lambda onClick. הערך של הפרמטר הזה הוא פונקציה, שהלחצן קורא לו כשהמשתמש לוחץ עליו:

Button(
    // ...
    onClick = myClickFunction
)
// ...

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

Button(
    // ...
    onClick = {
        // do something
        // do something else
    }
) { /* ... */ }

טראקים נגררים lambda

Kotlin מציעים תחביר מיוחד לקריאה לפונקציות מסדר גבוה יותר שהאחרונה שלהן היא הוא lambda. אם רוצים להעביר ביטוי lambda אפשר להשתמש ב-trailing lambda תחביר. במקום להוסיף את ביטוי lambda בתוך הסוגריים, לאחר מכן. זהו מצב נפוץ בניסוח האוטומטי, לכן צריך להכיר במראה של הקוד.

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

Column(
    modifier = Modifier.padding(16.dp),
    content = {
        Text("Some text")
        Text("Some more text")
        Text("Last text")
    }
)

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

Column(modifier = Modifier.padding(16.dp)) {
    Text("Some text")
    Text("Some more text")
    Text("Last text")
}

לשתי הדוגמאות יש משמעות זהה לחלוטין. המסגרת מסמלת את ה-lambda ביטוי שמועבר לפרמטר content.

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

Column {
    Text("Some text")
    Text("Some more text")
    Text("Last text")
}

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

היקפי הרשאות ומקלטים

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

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

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

לרוב, קוד כזה יופיע בפונקציות של הכתיבה:

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, קל להצהיר על Singletons, מחלקות שתמיד יש להן אחד רק מופע אחד. הסינגלונים האלה מוצהרים באמצעות מילת המפתח object. לעיתים קרובות משתמשים באובייקטים כאלה ב-Compose. לדוגמה, MaterialTheme הוא מוגדר כאובייקט סינגלטון; MaterialTheme.colors, shapes המאפיינים של typography מכילים את הערכים של העיצוב הנוכחי.

בנאים ופרוטוקולי DSL בטוחים לסוג

Kotlin מאפשרים ליצור שפות ספציפיות לדומיין (DSL) בעזרת builderים בטוחים לסוג. DSL מאפשרים לבנות נתונים היררכיים מורכבים את המבנה שלהם בצורה קריאה וקריאה יותר.

Jetpack פיתוח נייטיב משתמש ב-DSL עבור ממשקי 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 מבטיחה שהבנייה של סוג מסוים תהיה בטוחה באמצעות ליטרלים של פונקציות עם המקלט. אם ניקח את 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 ב: המסמכים של קוטלין.

שגרת קוטלין (Kotlin)

Coroutines מציע תמיכה בתכנות אסינכרוני ברמת השפה Kotlin. קורוטינים יכולים להשעות הפעלה בלי לחסום שרשורים. א' ממשק משתמש רספונסיבי הוא מטבעו אסינכרוני, ו-Jetpack פיתוח נייטיב פותר את הבעיה מאמצים קורוטין ברמת ה-API במקום להשתמש בקריאות חוזרות (callback).

ב-Jetpack פיתוח נייטיב יש ממשקי API שהופכים את השימוש בקורוטינים לבטוח בשכבת ממשק המשתמש. rememberCoroutineScope הפונקציה מחזירה CoroutineScope שבעזרתה אפשר ליצור קורוטינים בגורמים מטפלים באירועים ובקריאה כתיבת ממשקי API של השעיה. אפשר לראות את הדוגמה הבאה בעזרת של ScrollState API של animateScrollTo.

// 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()
        }
    }
) { /* ... */ }

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

למידע נוסף על קורוטין, אפשר לעיין המדריך בנושא קורוטין של קוטלין ב-Android.