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 یک پارامتر لامبدا 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 در اندروید مراجعه کنید.

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