‫Kotlin برای Jetpack Compose

‫Jetpack Compose براساس Kotlin ساخته شده است. در برخی موارد، Kotlin اصطلاحات ویژه‌ای ارائه می‌دهد که نوشتن کد Compose خوب را آسان‌تر می‌کند. اگر به زبان برنامه‌نویسی دیگری فکر می‌کنید و آن زبان را ذهنی به Kotlin ترجمه می‌کنید، احتمالاً برخی‌از قدرت‌های Compose را ازدست می‌دهید و ممکن است درک کد Kotlin که به‌صورت اصطلاحی نوشته شده است برایتان دشوار باشد. آشنایی بیشتر با سبک Kotlin می‌تواند به شما کمک کند از این مشکلات جلوگیری کنید.

آرگومان‌های پیش‌فرض

وقتی تابع Kotlin می‌نویسید، می‌توانید مقادیر پیش‌فرض برای آرگومان‌های تابع مشخص کنید که اگر فراخواننده آن مقادیر را به‌طور صریح ارسال نکند، استفاده می‌شوند. این ویژگی نیاز به توابع بیش‌ازحد بارگذاری‌شده را کاهش می‌دهد.

برای مثال، فرض کنید می‌خواهید تابعی بنویسید که یک مربع رسم می‌کند. آن تابع ممکن است یک پارامتر الزامی واحد، sideLength، داشته باشد که طول هر ضلع را مشخص می‌کند. ممکن است چندین پارامتر اختیاری داشته باشد، مانند ضخامت، رنگ لبه، و غیره؛ اگر تماس‌گیرنده آن‌ها را مشخص نکند، تابع از مقادیر پیش‌فرض استفاده می‌کند. در زبان‌های دیگر، ممکن است انتظار داشته باشید چند تابع بنویسید:

// 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، مستند می‌کنید که برای همه پارامترهای دیگر، می‌خواهید از مقادیر پیش‌فرض استفاده کنید. درمقابل، تکه‌کد دوم نشان می‌دهد که می‌خواهید مقادیر آن پارامترهای دیگر را به‌طور صریح تنظیم کنید، هرچند مقادیری که تنظیم می‌کنید اتفاقاً مقادیر پیش‌فرض تابع هستند.

توابع مرتبه بالاتر و عبارات لامبدا

‫Kotlin از توابع مرتبه بالاتر، توابعی که توابع دیگر را به‌عنوان پارامتر دریافت می‌کنند، پشتیبانی می‌کند. ‫Compose براساس این رویکرد ساخته شده است. برای مثال، تابع ترکیب‌پذیر Button پارامتر onClick لامبدا را ارائه می‌دهد. مقدار آن پارامتر تابعی است که وقتی کاربر روی دکمه کلیک می‌کند، دکمه آن را فرا می‌خواند:

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

توابع مرتبه بالاتر به‌طور طبیعی با عبارات لامبدا، عباراتی که به تابع ارزیابی می‌شوند، جفت می‌شوند. اگر فقط یک‌بار به تابع نیاز دارید، لازم نیست آن را در جای دیگری تعریف کنید تا به تابع مرتبه بالاتر منتقل کنید. درعوض، می‌توانید تابع را همان‌جا با عبارت لامبدا تعریف کنید. مثال قبلی فرض می‌کند که myClickFunction() در جای دیگری تعریف شده است. اما اگر فقط از آن تابع در اینجا استفاده می‌کنید، ساده‌تر است که تابع را به‌صورت درون‌خطی با عبارت لامبدا تعریف کنید:

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

لامبداهای انتهایی

‫Kotlin برای فراخوانی توابع مرتبه بالاتری که آخرین پارامتر آن‌ها لامبدا است، دستور ویژه‌ای ارائه می‌دهد. اگر می‌خواهید عبارت لامبدا را به‌عنوان آن پارامتر ارسال کنید، می‌توانید از نحو لامبدای دنباله‌دار استفاده کنید. به‌جای اینکه عبارت لامبدا را درون پرانتز قرار دهید، آن را بعداز پرانتز قرار می‌دهید. این وضعیت در Compose رایج است، بنابراین باید با ظاهر کد آشنا باشید.

برای مثال، آخرین پارامتر برای همه چیدمان‌ها، مثل تابع 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")
}

این دو مثال دقیقاً یک معنی دارند. آکولادها عبارت لامبدا را که به پارامتر content ارسال می‌شود تعریف می‌کنند.

درواقع، اگر تنها پارامتری که می‌گذرانید آن لامبدای انتهایی باشد—یعنی، اگر پارامتر نهایی لامبدا باشد و هیچ پارامتر دیگری را نمی‌گذرانید—می‌توانید پرانتزها را به‌طور کامل حذف کنید. بنابراین، برای مثال، فرض کنید نیازی به ارسال اصلاح‌گر به Column ندارید. می‌توانید کد را به‌صورت زیر بنویسید:

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

این دستورگان در «نوشتن» بسیار رایج است، به‌ویژه برای عناصر چیدمان مانند Column. آخرین پارامتر یک عبارت لامبدا است که فرزندان عنصر را تعریف می‌کند، و آن فرزندان در آکولاد پس‌از فراخوانی تابع مشخص می‌شوند.

محدوده‌ها و گیرنده‌ها

برخی‌از روش‌ها و دارایی‌ها فقط در محدوده خاصی دردسترس هستند. محدوده محدود به شما امکان می‌دهد عملکرد را در جایی که نیاز است ارائه دهید و از استفاده تصادفی از آن عملکرد در جایی که مناسب نیست جلوگیری کنید.

مثالی را که در «نوشتن» استفاده می‌شود درنظر بگیرید. وقتی چیدمان 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)
    )
}

برخی‌از «میاناهای برنامه‌سازی کاربردی» لامبداهایی را می‌پذیرند که در محدوده گیرنده فراخوانی می‌شوند. آن لامبداها براساس اعلان پارامتر به دارایی‌ها و توابعی که در جای دیگری تعریف شده‌اند دسترسی دارند:

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

درحال ازهم بازکردن کلاس‌های داده

اگر کلاس داده را تعریف کنید، می‌توانید با اعلان ساختارشکن به داده‌ها دسترسی پیدا کنید. برای مثال، فرض کنید کلاس 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() را تعریف می‌کند. در اسناد کلاس‌های داده می‌توانید اطلاعات بیشتری پیدا کنید.

اشیا تک‌نمونه

‫Kotlin به شما امکان می‌دهد تک‌نمونه‌ها را تعریف کنید، کلاس‌هایی که همیشه یک و فقط یک نمونه دارند. این تک‌نمونه‌ها با کلیدواژه object تعریف می‌شوند. ‫Compose اغلب از چنین اشیایی استفاده می‌کند. برای مثال، MaterialTheme به‌عنوان یک شیء تک‌موردی تعریف شده است؛ دارایی‌های MaterialTheme.colors، shapes، و typography همه حاوی مقادیر موضوع کنونی هستند.

سازندگان و زبان‌های نشانه‌گذاری قابل‌اعتماد

‫Kotlin امکان ایجاد زبان‌های خاص دامنه (DSLs) با سازندگان ایمن نوع را فراهم می‌کند. زبان‌های DSL امکان ساخت ساختارهای داده سلسله‌مراتبی پیچیده را به روشی قابل‌نگهداری و خواناتر فراهم می‌کنند.

‫Jetpack Compose برای برخی‌از APIها مثل LazyRow و LazyColumn از DSL استفاده می‌کند.

@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)
        }
    }
}

در اسناد Kotlin درباره سازندگان نوع امن و DSL بیشتر بدانید.

روال‌های مشترک Kotlin

روال‌های همکار در Kotlin از برنامه‌نویسی ناهم‌زمان در سطح زبان پشتیبانی می‌کنند. روال‌های همکار می‌توانند اجرای خود را بدون مسدود کردن رشته‌ها تعلیق کنند. میانای کاربر پاسخ‌گو ذاتاً ناهم‌زمان است و Jetpack Compose این مشکل را با پذیرفتن روتین‌های همکار در سطح میانای برنامه‌سازی کاربردی به‌جای استفاده از برگشتی‌ها حل می‌کند.

‫Jetpack Compose میاناهای برنامه‌سازی کاربردی‌ای ارائه می‌دهد که استفاده از روال‌های مشترک را در لایه میانای کاربر ایمن می‌کند. تابع rememberCoroutineScope یک CoroutineScope برمی‌گرداند که با آن می‌توانید در مدیریت‌کننده‌های رویداد روال‌های مشترک ایجاد کنید و میاناهای برنامه‌سازی کاربردی تعلیق Compose را فراخوانی کنید. مثال زیر را بااستفاده از ScrollState میانای برنامه‌سازی کاربردی 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()
        }
    }
) { /* ... */ }

به‌طور پیش‌فرض، روتین‌های همکار بلوک کد را به‌صورت ترتیبی اجرا می‌کنند. روال هم‌زمان درحال اجرا که تابع تعلیق را فرا می‌خواند، اجرای خود را تا زمانی که تابع تعلیق برگردد تعلیق می‌کند. این امر حتی اگر تابع تعلیق اجرا را به 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()
        }
    }
) { /* ... */ }

روال‌های همکار ترکیب کردن «میاناهای برنامه‌سازی کاربردی» ناهمزمان را آسان‌تر می‌کنند. در مثال زیر، اصلاح‌گر 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 and animate
                        // in the same block
                        awaitPointerEventScope {
                            val offset = 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 را بررسی کنید.