Kotlin برای Jetpack Compose

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

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

هنگامی که یک تابع 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)

اکثر کتابخانه‌های Compose از آرگومان‌های پیش‌فرض استفاده می‌کنند، و انجام همین کار برای توابع ترکیب‌پذیری که می‌نویسید، تمرین خوبی است. این عمل باعث می‌شود که composable‌های شما قابل تنظیم باشند، اما همچنان رفتار پیش‌فرض را برای فراخوانی ساده می‌کند. بنابراین، برای مثال، ممکن است یک عنصر متن ساده مانند این ایجاد کنید:

Text(text = "Hello, Android!")

این کد همان اثری را دارد که کد زیر بسیار پرمخاطب تر است، که در آن پارامترهای Text به صراحت تنظیم شده است:

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

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

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

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

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

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

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

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

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

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

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

برخی از 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 با گیرنده در مستندات 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 سازنده های ایمن با استفاده از تابع literals با گیرنده را تضمین می کند. اگر 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)
        }
    }
}

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

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

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

Jetpack Compose API هایی را ارائه می دهد که استفاده از برنامه های مشترک را در لایه UI ایمن می کند. تابع rememberCoroutineScope یک CoroutineScope برمی‌گرداند که با آن می‌توانید کوروتین‌هایی را در کنترل‌کننده‌های رویداد ایجاد کنید و Compose suspend APIs را فراخوانی کنید. مثال زیر را با استفاده از ScrollState animateScrollTo API ببینید.

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

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

Coroutine ها ترکیب 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)
        )
    }

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

{% کلمه به کلمه %} {% آخر کلمه %} {% کلمه به کلمه %} {% endverbatim %}،

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

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

هنگامی که یک تابع 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)

اکثر کتابخانه‌های Compose از آرگومان‌های پیش‌فرض استفاده می‌کنند، و انجام همین کار برای توابع ترکیب‌پذیری که می‌نویسید، تمرین خوبی است. این عمل باعث می‌شود که composable‌های شما قابل تنظیم باشند، اما همچنان رفتار پیش‌فرض را برای فراخوانی ساده می‌کند. بنابراین، برای مثال، ممکن است یک عنصر متن ساده مانند این ایجاد کنید:

Text(text = "Hello, Android!")

این کد همان اثری را دارد که کد زیر بسیار پرمخاطب تر است، که در آن پارامترهای Text به صراحت تنظیم شده است:

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

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

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

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

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

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

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

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

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

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

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

برخی از 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 با گیرنده در مستندات 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 سازنده های ایمن با استفاده از تابع literals با گیرنده را تضمین می کند. اگر 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)
        }
    }
}

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

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

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

Jetpack Compose API هایی را ارائه می دهد که استفاده از برنامه های مشترک را در لایه UI ایمن می کند. تابع rememberCoroutineScope یک CoroutineScope برمی‌گرداند که با آن می‌توانید کوروتین‌هایی را در کنترل‌کننده‌های رویداد ایجاد کنید و Compose suspend APIs را فراخوانی کنید. مثال زیر را با استفاده از ScrollState animateScrollTo API ببینید.

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

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

Coroutine ها ترکیب 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)
        )
    }

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

{% کلمه به کلمه %} {% آخر کلمه %} {% کلمه به کلمه %} {% endverbatim %}،

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

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

هنگامی که یک تابع 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)

اکثر کتابخانه‌های Compose از آرگومان‌های پیش‌فرض استفاده می‌کنند، و انجام همین کار برای توابع ترکیب‌پذیری که می‌نویسید، تمرین خوبی است. این عمل باعث می‌شود که composable‌های شما قابل تنظیم باشند، اما همچنان رفتار پیش‌فرض را برای فراخوانی ساده می‌کند. بنابراین، برای مثال، ممکن است یک عنصر متن ساده مانند این ایجاد کنید:

Text(text = "Hello, Android!")

این کد همان اثری را دارد که کد زیر بسیار پرمخاطب تر است، که در آن پارامترهای Text به صراحت تنظیم شده است:

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

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

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

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

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

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

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

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

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

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

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

برخی از 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 با گیرنده در مستندات 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 سازنده های ایمن با استفاده از تابع literals با گیرنده را تضمین می کند. اگر 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)
        }
    }
}

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

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

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

Jetpack Compose API هایی را ارائه می دهد که استفاده از برنامه های مشترک را در لایه UI ایمن می کند. تابع rememberCoroutineScope یک CoroutineScope برمی‌گرداند که با آن می‌توانید کوروتین‌هایی را در کنترل‌کننده‌های رویداد ایجاد کنید و Compose suspend APIs را فراخوانی کنید. مثال زیر را با استفاده از ScrollState animateScrollTo API ببینید.

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

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

Coroutine ها ترکیب 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)
        )
    }

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

{% کلمه به کلمه %} {% آخر کلمه %} {% کلمه به کلمه %} {% endverbatim %}،

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

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

هنگامی که یک تابع 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)

اکثر کتابخانه‌های Compose از آرگومان‌های پیش‌فرض استفاده می‌کنند، و انجام همین کار برای توابع ترکیب‌پذیری که می‌نویسید، تمرین خوبی است. این عمل باعث می‌شود که composable‌های شما قابل تنظیم باشند، اما همچنان رفتار پیش‌فرض را برای فراخوانی ساده می‌کند. بنابراین، برای مثال، ممکن است یک عنصر متن ساده مانند این ایجاد کنید:

Text(text = "Hello, Android!")

این کد همان اثری را دارد که کد زیر بسیار پرمخاطب تر است، که در آن پارامترهای Text به صراحت تنظیم شده است:

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

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

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

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

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

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

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

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

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

به عنوان مثال ، آخرین پارامتر برای همه طرح بندی ها ، مانند عملکرد Column() Composable ، content است ، تابعی که عناصر UI کودک را منتشر می کند. فرض کنید می خواستید ستونی را که شامل سه عنصر متن است ، ایجاد کنید و باید برخی از قالب بندی ها را اعمال کنید. این کد کار می کند ، اما بسیار دشوار است:

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

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

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

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

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

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

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

مثالی را که در آهنگسازی استفاده می شود در نظر بگیرید. هنگامی که شما به عنوان Row Layout Composive تماس می گیرید ، محتوای شما 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(
            /*...*/
            /* ...
        )
    }
)

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

خواص واگذار شده

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

اشیاء Singleton

کوتلین اعلام تک آهنگ ها ، کلاس هایی را که همیشه یک و تنها یک نمونه دارند ، آسان می کند. این تک آهنگ ها با کلمه کلیدی object اعلام می شوند. آهنگسازی اغلب از چنین اشیاء استفاده می کند. به عنوان مثال ، MaterialTheme به عنوان یک شیء مجرد تعریف می شود. 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)
        }
    }
}

کوتلین سازندگان ایمن از نوع را با استفاده از لفظهای عملکردی با گیرنده تضمین می کند. اگر 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 در مستندات کوتلین اطلاعات بیشتری کسب کنید.

کوتلین کوروتینز

Coroutines پشتیبانی برنامه نویسی ناهمزمان را در سطح زبان در کوتلین ارائه می دهد. Coroutines می تواند اجرای را بدون مسدود کردن موضوعات به حالت تعلیق درآورد . UI پاسخگو ذاتاً ناهمزمان است ، و Jetpack به جای استفاده از تماس تلفنی ، این مسئله را با آغوش Coroutines در سطح API حل می کند.

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

Coroutines بلوک کد را به صورت متوالی به طور پیش فرض اجرا می کند. یک Coroutine در حال اجرا که یک عملکرد تعلیق را فراخوانی می کند ، اجرای آن را تا زمانی که عملکرد تعلیق بازگردد ، تعلیق می کند . این درست است حتی اگر عملکرد تعلیق اجرای آن را به یک CoroutineDispatcher متفاوت منتقل کند. در مثال قبلی ، loadData تا زمانی که عملکرد معلق animateScrollTo بازگردد ، اجرا نمی شود.

برای اجرای همزمان کد ، باید Coroutines جدید ایجاد شود. در مثال بالا ، برای موازی کردن پیمایش به بالای صفحه و بارگیری داده ها از viewModel ، به دو Coroutines مورد نیاز است.

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

Coroutines ترکیب 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)
        )
    }

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

{% کلمه به کلمه %} {% آخر کلمه %} {% کلمه به کلمه %} {% آخر کلمه %}