Kotlin برای Jetpack Compose

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

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

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

برای مثال، فرض کنید می‌خواهید تابعی بنویسید که یک مربع رسم کند. آن تابع ممکن است یک پارامتر اجباری به نام 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) { }

در کاتلین، می‌توانید یک تابع واحد بنویسید و مقادیر پیش‌فرض را برای آرگومان‌ها مشخص کنید:

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

Text(text = "Hello, Android!")

این کد همان تأثیر کد زیر را دارد، کدی که بسیار مفصل‌تر است و در آن پارامترهای Text بیشتری به صراحت تنظیم شده‌اند:

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

نه تنها قطعه کد اول بسیار ساده‌تر و خواناتر است، بلکه خود-مستندسازی نیز می‌کند. با مشخص کردن فقط پارامتر text ، شما مستند می‌کنید که برای سایر پارامترها، می‌خواهید از مقادیر پیش‌فرض استفاده کنید. در مقابل، قطعه کد دوم دلالت بر این دارد که شما می‌خواهید مقادیر سایر پارامترها را به صراحت تنظیم کنید، اگرچه مقادیری که تعیین می‌کنید، مقادیر پیش‌فرض برای تابع هستند.

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

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

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

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

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

لامبداهای دنباله‌دار

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

برای مثال، آخرین پارامتر برای همه طرح‌بندی‌ها، مانند تابع composable 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 layout composable را فراخوانی می‌کنید، content 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ها، لامبداهایی را می‌پذیرند که در محدوده گیرنده فراخوانی می‌شوند. این لامبداها به ویژگی‌ها و توابعی که در جای دیگری تعریف شده‌اند، بر اساس تعریف پارامتر، دسترسی دارند:

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

برای اطلاعات بیشتر، به بخش «literals توابع با receiver» در مستندات کاتلین مراجعه کنید.

املاک واگذار شده

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

class DelegatingClass {
    var name: String by nameGetterFunction()

    // ...
}

کدهای دیگر می‌توانند با کدی مانند این به خاصیت دسترسی پیدا کنند:

val myDC = DelegatingClass()
println("The name property is: " + myDC.name)

وقتی println() اجرا می‌شود، nameGetterFunction() فراخوانی می‌شود تا مقدار رشته را برگرداند.

این ویژگی‌های واگذار شده به ویژه زمانی مفید هستند که با ویژگی‌های دارای پشتیبانی state کار می‌کنید:

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

اشیاء سینگلتون

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

سازندگان و DSL های ایمن از نوع

کاتلین امکان ایجاد زبان‌های خاص دامنه (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)
        }
    }
}

کاتلین با استفاده از تابع‌های لیترال به همراه receiver ، سازنده‌های type-safe را تضمین می‌کند. اگر Canvas composable را به عنوان مثال در نظر بگیریم، به عنوان پارامتر، تابعی با 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ها به مستندات کاتلین مراجعه کنید.

کوروتین‌های کاتلین

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

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

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

برای کسب اطلاعات بیشتر در مورد Coroutineها، به راهنمای Kotlin Coroutineها در اندروید مراجعه کنید.

{% کلمه به کلمه %} {% فعل کمکی %} {% کلمه به کلمه %} {% فعل کمکی %}