‫Kotlin برای Jetpack Compose

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

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

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

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

توابع مرتبه بالاتر به‌طور طبیعی با عبارات لامبدا جفت می‌شوند، عباراتی که به یک تابع ارزیابی می‌شوند. اگر فقط یک‌بار به تابع نیاز دارید، لازم نیست آن را در جای دیگری تعریف کنید تا به تابع مرتبه بالاتر ارسال شود. درعوض، می‌توانید تابع را درست در همان‌جا با عبارت lambda تعریف کنید. مثال قبلی فرض می‌کند که 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")
}

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

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

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

نمونه‌ای را که در Compose استفاده شده است درنظر بگیرید. وقتی ترکیب‌پذیر چیدمان 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

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

اشیای الگوی یگانه

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

سازندگان ایمن ازنظر نوع و زبان‌های خاص دامنه (DSL)

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

‫Jetpack Compose از 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 بیشتر بدانید.

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

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

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

روال‌های همکار ترکیب کردن میاناهای برنامه‌سازی کاربردی ناهمگام را آسان‌تر می‌کند. در مثال زیر، اصلاح‌کننده pointerInput را با میاناهای برنامه‌سازی کاربردی پویانمایی ترکیب می‌کنیم تا موقعیت عنصر را هنگام تک‌ضرب زدن کاربر روی صفحه پویانمایی کنیم.

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

برای کسب اطلاعات بیشتر درباره Coroutines، راهنمای روال‌های مشترک Kotlin در Android را بررسی کنید.