State و Jetpack Compose

وضعیت در یک برنامه هر مقداری است که می‌تواند در طول زمان تغییر کند. این یک تعریف بسیار گسترده است و همه چیز را از یک پایگاه داده Room گرفته تا یک متغیر در یک کلاس در بر می‌گیرد.

همه برنامه‌های اندروید وضعیت (state) را به کاربر نمایش می‌دهند. چند نمونه از وضعیت‌ها در برنامه‌های اندروید:

  • یک اسنک‌بار که نشان می‌دهد چه زمانی اتصال شبکه برقرار نمی‌شود.
  • یک پست وبلاگ و نظرات مرتبط.
  • انیمیشن‌های موج‌دار روی دکمه‌هایی که هنگام کلیک کاربر روی آنها پخش می‌شوند.
  • استیکرهایی که کاربر می‌تواند روی یک تصویر بکشد.

Jetpack Compose به شما کمک می‌کند تا در مورد محل و نحوه ذخیره و استفاده از state در یک برنامه اندروید، صریح باشید. این راهنما بر ارتباط بین state و composableها و APIهایی که Jetpack Compose برای کار آسان‌تر با state ارائه می‌دهد، تمرکز دارد.

حالت و ترکیب

Compose اعلانی است و به همین دلیل تنها راه برای به‌روزرسانی آن، فراخوانی همان composable با آرگومان‌های جدید است. این آرگومان‌ها نمایانگر وضعیت رابط کاربری هستند. هر بار که یک وضعیت به‌روزرسانی می‌شود، یک recomposition اتفاق می‌افتد. در نتیجه، چیزهایی مانند TextField مانند آنچه در نماهای مبتنی بر XML دستوری انجام می‌شود، به‌طور خودکار به‌روزرسانی نمی‌شوند. یک composable باید به‌طور صریح وضعیت جدید را به آن اطلاع دهد تا به‌طور متناسب به‌روزرسانی شود.

@Composable
private fun HelloContent() {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello!",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.bodyMedium
        )
        OutlinedTextField(
            value = "",
            onValueChange = { },
            label = { Text("Name") }
        )
    }
}

اگر این را اجرا کنید و سعی کنید متنی وارد کنید، خواهید دید که هیچ اتفاقی نمی‌افتد. دلیلش این است که TextField خودش را به‌روزرسانی نمی‌کند - وقتی پارامتر value تغییر کند، به‌روزرسانی می‌شود. این به دلیل نحوه‌ی عملکرد ترکیب و ترکیب مجدد در Compose است.

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

حالت در ترکیبات

توابع Composable می‌توانند از API remember برای ذخیره یک شیء در حافظه استفاده کنند. مقداری که توسط remember محاسبه می‌شود، در طول ترکیب اولیه در Composition ذخیره می‌شود و مقدار ذخیره شده در طول ترکیب مجدد بازگردانده می‌شود. remember می‌تواند برای ذخیره اشیاء تغییرپذیر و تغییرناپذیر استفاده شود.

mutableStateOf یک MutableState<T> قابل مشاهده ایجاد می‌کند که یک نوع قابل مشاهده است که با زمان اجرای compose ادغام شده است.

interface MutableState<T> : State<T> {
    override var value: T
}

هرگونه تغییر در زمان‌بندی‌های value ، ترکیب مجدد توابع قابل ترکیبی که value می‌خوانند، را انجام می‌دهد.

سه راه برای تعریف یک شیء MutableState در یک composable وجود دارد:

  • val mutableState = remember { mutableStateOf(default) }
  • var value by remember { mutableStateOf(default) }
  • val (value, setValue) = remember { mutableStateOf(default) }

این اعلان‌ها معادل هستند و به عنوان syntax sugar برای کاربردهای مختلف state ارائه می‌شوند. شما باید آن را انتخاب کنید که کد خواناتری را در composable که می‌نویسید، تولید کند.

سینتکس by delegate به ایمپورت‌های زیر نیاز دارد:

import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

شما می‌توانید از مقدار به خاطر سپرده شده به عنوان پارامتر برای سایر composableها یا حتی به عنوان منطق در دستورات برای تغییر اینکه کدام composableها نمایش داده شوند، استفاده کنید. برای مثال، اگر نمی‌خواهید در صورت خالی بودن name، پیام خوشامدگویی نمایش داده شود، از state در یک دستور if استفاده کنید:

@Composable
fun HelloContent() {
    Column(modifier = Modifier.padding(16.dp)) {
        var name by remember { mutableStateOf("") }
        if (name.isNotEmpty()) {
            Text(
                text = "Hello, $name!",
                modifier = Modifier.padding(bottom = 8.dp),
                style = MaterialTheme.typography.bodyMedium
            )
        }
        OutlinedTextField(
            value = name,
            onValueChange = { name = it },
            label = { Text("Name") }
        )
    }
}

اگرچه remember به شما کمک می‌کند تا state را در طول recompositionها حفظ کنید، اما state در طول تغییرات پیکربندی حفظ نمی‌شود. برای این کار، باید rememberSaveable استفاده کنید. rememberSaveable به طور خودکار هر مقداری را که می‌توان در یک Bundle ذخیره کرد، ذخیره می‌کند. برای مقادیر دیگر، می‌توانید یک شیء saver سفارشی ارسال کنید.

سایر انواع پشتیبانی‌شده‌ی وضعیت

Compose نیازی به استفاده از MutableState<T> برای نگهداری state ندارد؛ از سایر انواع observable نیز پشتیبانی می‌کند. قبل از خواندن یک نوع observable دیگر در Compose، باید آن را به State<T> تبدیل کنید تا composableها بتوانند هنگام تغییر state به طور خودکار دوباره ترکیب شوند.

Compose توابعی را برای ایجاد State<T> از انواع قابل مشاهده رایج مورد استفاده در برنامه‌های اندروید ارائه می‌دهد. قبل از استفاده از این ادغام‌ها، مصنوعات (یا مصنوعات) مناسب را همانطور که در زیر آمده است اضافه کنید:

  • Flow : collectAsStateWithLifecycle()

    collectAsStateWithLifecycle() مقادیر را از یک Flow به شیوه‌ای آگاه از چرخه حیات (lifecycle-aware) جمع‌آوری می‌کند و به برنامه شما اجازه می‌دهد تا منابع برنامه را حفظ کند. این تابع آخرین مقدار منتشر شده از State Compose را نشان می‌دهد. از این API به عنوان روش پیشنهادی برای جمع‌آوری جریان‌ها در برنامه‌های اندروید استفاده کنید.

    وابستگی زیر در فایل build.gradle مورد نیاز است (باید نسخه 2.6.0-beta01 یا جدیدتر باشد):

کاتلین

dependencies {
      ...
      implementation("androidx.lifecycle:lifecycle-runtime-compose:2.9.4")
}

گرووی

dependencies {
      ...
      implementation "androidx.lifecycle:lifecycle-runtime-compose:2.9.4"
}
  • Flow : collectAsState()

    collectAsState مشابه collectAsStateWithLifecycle است، زیرا مقادیر را از یک Flow جمع‌آوری کرده و آن را به Compose State تبدیل می‌کند.

    برای کد مستقل از پلتفرم، به جای collectAsStateWithLifecycle که فقط برای اندروید است، از collectAsState استفاده کنید.

    وابستگی‌های اضافی برای collectAsState مورد نیاز نیست، زیرا در compose-runtime موجود است.

  • LiveData : observeAsState()

    observeAsState() شروع به مشاهده‌ی این LiveData می‌کند و مقادیر آن را از طریق State نمایش می‌دهد.

    وابستگی زیر در فایل build.gradle مورد نیاز است:

کاتلین

dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-livedata:1.9.3")
}

گرووی

dependencies {
      ...
      implementation "androidx.compose.runtime:runtime-livedata:1.9.3"
}

کاتلین

dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-rxjava2:1.9.3")
}

گرووی

dependencies {
      ...
      implementation "androidx.compose.runtime:runtime-rxjava2:1.9.3"
}

کاتلین

dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-rxjava3:1.9.3")
}

گرووی

dependencies {
      ...
      implementation "androidx.compose.runtime:runtime-rxjava3:1.9.3"
}

دارای تابعیت در مقابل بدون تابعیت

یک composable که از remember برای ذخیره یک شیء استفاده می‌کند، state داخلی ایجاد می‌کند و composable را stateful می‌کند. HelloContent نمونه‌ای از یک composable با stateful است زیرا name state خود را به صورت داخلی نگه می‌دارد و تغییر می‌دهد. این می‌تواند در موقعیت‌هایی مفید باشد که یک فراخواننده نیازی به کنترل state ندارد و می‌تواند بدون نیاز به مدیریت state خود از آن استفاده کند. با این حال، composableهایی با state داخلی، قابلیت استفاده مجدد کمتری دارند و آزمایش آنها دشوارتر است.

یک composable بدون وضعیت ، composable ای است که هیچ وضعیتی را نگه نمی‌دارد. یک راه آسان برای دستیابی به وضعیت بدون وضعیت، استفاده از state hoisting است.

همانطور که شما کامپوننت‌های قابل استفاده مجدد را توسعه می‌دهید، اغلب می‌خواهید هر دو نسخه با وضعیت (stateful) و بدون وضعیت (stateless) از یک کامپوننت را در معرض نمایش قرار دهید. نسخه با وضعیت (stateful) برای فراخوانی‌کنندگانی که به وضعیت (state) اهمیتی نمی‌دهند، مناسب است و نسخه بدون وضعیت (stateless) برای فراخوانی‌کنندگانی که نیاز به کنترل یا افزایش وضعیت (state) دارند، ضروری است.

بالابر دولتی

بالا بردن وضعیت در Compose الگویی برای انتقال وضعیت به فراخوانی‌کننده‌ی یک composable است تا یک composable را بی‌وضعیت کند. الگوی کلی برای بالا بردن وضعیت در Jetpack Compose جایگزینی متغیر state با دو پارامتر است:

  • value: T : مقدار فعلی برای نمایش
  • onValueChange: (T) -> Unit : رویدادی که درخواست تغییر مقدار را می‌دهد، که در آن T مقدار جدید پیشنهادی است.

با این حال، شما محدود به onValueChange نیستید. اگر رویدادهای خاص‌تری برای composable مناسب هستند، باید آنها را با استفاده از lambdas تعریف کنید.

حالتی که به این روش بالا برده می‌شود، چند ویژگی مهم دارد:

  • منبع واحد حقیقت: با جابجایی وضعیت به جای کپی کردن آن، مطمئن می‌شویم که فقط یک منبع حقیقت وجود دارد. این به جلوگیری از باگ‌ها کمک می‌کند.
  • کپسوله‌سازی شده: فقط کامپوننت‌های stateful می‌توانند وضعیت خود را تغییر دهند. این کاملاً داخلی است.
  • قابلیت اشتراک‌گذاری: حالت Hoist شده را می‌توان با چندین composable به اشتراک گذاشت. اگر می‌خواستید name در یک composable متفاوت بخوانید، hoisting به شما این امکان را می‌دهد.
  • قابل رهگیری: فراخوانی‌کنندگان به composableهای بدون وضعیت می‌توانند تصمیم بگیرند که رویدادها را نادیده بگیرند یا قبل از تغییر وضعیت، آنها را تغییر دهند.
  • جدا شده: وضعیت کامپوننت‌های بدون وضعیت می‌تواند در هر جایی ذخیره شود. برای مثال، اکنون می‌توان name به ViewModel منتقل کرد.

در مثال، شما name و onValueChange از HelloContent استخراج می‌کنید و آنها را در درخت به یک کامپوننت HelloScreen که HelloContent فراخوانی می‌کند، منتقل می‌کنید.

@Composable
fun HelloScreen() {
    var name by rememberSaveable { mutableStateOf("") }

    HelloContent(name = name, onNameChange = { name = it })
}

@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello, $name",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.bodyMedium
        )
        OutlinedTextField(value = name, onValueChange = onNameChange, label = { Text("Name") })
    }
}

با انتقال وضعیت از HelloContent ، استدلال در مورد composable، استفاده مجدد از آن در موقعیت‌های مختلف و آزمایش آن آسان‌تر می‌شود. HelloContent از نحوه ذخیره وضعیت خود جدا شده است. جداسازی به این معنی است که اگر HelloScreen تغییر دهید یا جایگزین کنید، لازم نیست نحوه پیاده‌سازی HelloContent را تغییر دهید.

الگویی که در آن وضعیت (state) کاهش و رویدادها (event) افزایش می‌یابند، جریان داده یک‌طرفه نامیده می‌شود. در این حالت، وضعیت از HelloScreen به HelloContent کاهش و رویدادها از HelloContent به HelloScreen افزایش می‌یابند. با پیروی از جریان داده یک‌طرفه، می‌توانید composableهایی که وضعیت را در رابط کاربری نمایش می‌دهند، از بخش‌هایی از برنامه که وضعیت را ذخیره و تغییر می‌دهند، جدا کنید.

برای کسب اطلاعات بیشتر به صفحه «کجا وضعیت را بالا ببریم» مراجعه کنید.

بازیابی وضعیت در Compose

API مربوط به rememberSaveable مشابه remember رفتار می‌کند، زیرا وضعیت را در طول ترکیب‌های مجدد و همچنین در طول بازآفرینی فعالیت یا فرآیند با استفاده از مکانیسم وضعیت نمونه ذخیره شده، حفظ می‌کند. برای مثال، این اتفاق زمانی می‌افتد که صفحه نمایش چرخانده شود.

روش‌های ذخیره حالت

تمام انواع داده‌هایی که به Bundle اضافه می‌شوند، به‌طور خودکار ذخیره می‌شوند. اگر می‌خواهید چیزی را ذخیره کنید که نمی‌توان آن را به Bundle اضافه کرد، چندین گزینه وجود دارد.

بسته بندی

ساده‌ترین راه حل، اضافه کردن حاشیه‌نویسی @Parcelize به شیء است. شیء قابلیت بسته‌بندی پیدا می‌کند و می‌توان آن را دسته‌بندی کرد. برای مثال، این کد یک نوع داده‌ی parcelable City می‌سازد و آن را در state ذخیره می‌کند.

@Parcelize
data class City(val name: String, val country: String) : Parcelable

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

نقشه‌بردار

اگر به هر دلیلی @Parcelize مناسب نباشد، می‌توانید از mapSaver برای تعریف قانون خودتان برای تبدیل یک شیء به مجموعه‌ای از مقادیر که سیستم می‌تواند در Bundle ذخیره کند، استفاده کنید.

data class City(val name: String, val country: String)

val CitySaver = run {
    val nameKey = "Name"
    val countryKey = "Country"
    mapSaver(
        save = { mapOf(nameKey to it.name, countryKey to it.country) },
        restore = { City(it[nameKey] as String, it[countryKey] as String) }
    )
}

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

لیست سیور

برای جلوگیری از نیاز به تعریف کلید برای نقشه، می‌توانید listSaver نیز استفاده کنید و از اندیس‌های آن به عنوان کلید استفاده کنید:

data class City(val name: String, val country: String)

val CitySaver = listSaver<City, Any>(
    save = { listOf(it.name, it.country) },
    restore = { City(it[0] as String, it[1] as String) }
)

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

دارندگان امتیاز ایالتی در Compose

بالا بردن ساده‌ی وضعیت (state hoisting) را می‌توان در خود توابع composable مدیریت کرد. با این حال، اگر مقدار وضعیتی که باید پیگیری شود افزایش یابد، یا منطقی که باید در توابع composable اجرا شود، مطرح شود، بهتر است مسئولیت‌های منطق و وضعیت را به کلاس‌های دیگر واگذار کنید: دارندگان وضعیت (state holders ).

برای کسب اطلاعات بیشتر، به بخش «بالا بردن وضعیت» در مستندات Compose یا به طور کلی‌تر، به صفحه «نگهدارنده‌های وضعیت و وضعیت رابط کاربری» در راهنمای معماری مراجعه کنید.

فعال‌کننده‌ی مجدد، محاسبات را هنگام تغییر کلیدها به خاطر می‌سپارد

API remember اغلب همراه با MutableState استفاده می‌شود:

var name by remember { mutableStateOf("") }

در اینجا، استفاده از تابع remember باعث می‌شود مقدار MutableState از ترکیب‌های مجدد جان سالم به در ببرد.

به طور کلی، remember یک پارامتر calculation lambda می‌گیرد. وقتی remember برای اولین بار اجرا می‌شود، lambda calculation فراخوانی کرده و نتیجه آن را ذخیره می‌کند. در طول ترکیب مجدد، remember مقداری را که آخرین بار ذخیره شده بود، برمی‌گرداند.

جدا از وضعیت ذخیره‌سازی، می‌توانید remember برای ذخیره هر شیء یا نتیجه عملیاتی در Composition که مقداردهی اولیه یا محاسبه آن پرهزینه است نیز استفاده کنید. ممکن است نخواهید این محاسبه را در هر recomposition تکرار کنید. به عنوان مثال، ایجاد این شیء ShaderBrush که یک عملیات پرهزینه است:

val brush = remember {
    ShaderBrush(
        BitmapShader(
            ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(),
            Shader.TileMode.REPEAT,
            Shader.TileMode.REPEAT
        )
    )
}

remember مقدار را تا زمانی که از Composition خارج شود، ذخیره می‌کند. با این حال، راهی برای نامعتبر کردن مقدار ذخیره شده در حافظه پنهان وجود دارد. API remember همچنین یک پارامتر key یا keys را دریافت می‌کند. اگر هر یک از این کلیدها تغییر کنند، دفعه بعد که تابع recompose می‌شود ، remember حافظه پنهان را نامعتبر می‌کند و بلوک لامبدا محاسبه را دوباره اجرا می‌کند . این مکانیسم به شما امکان کنترل طول عمر یک شیء در Composition را می‌دهد. محاسبه تا زمانی که ورودی‌ها تغییر کنند، معتبر باقی می‌ماند، نه تا زمانی که مقدار ذخیره شده از Composition خارج شود.

مثال‌های زیر نحوه‌ی عملکرد این مکانیزم را نشان می‌دهند.

در این قطعه کد، یک ShaderBrush ایجاد شده و به عنوان رنگ پس‌زمینه یک Box composable استفاده می‌شود. remember نمونه ShaderBrush ذخیره می‌کند زیرا همانطور که قبلاً توضیح داده شد، ایجاد مجدد آن پرهزینه است. remember که avatarRes به عنوان پارامتر key1 می‌گیرد که تصویر پس‌زمینه انتخاب شده است. اگر avatarRes تغییر کند، قلم‌مو با تصویر جدید دوباره ترکیب می‌شود و دوباره به Box اعمال می‌شود. این می‌تواند زمانی رخ دهد که کاربر تصویر دیگری را به عنوان پس‌زمینه از یک انتخابگر انتخاب کند.

@Composable
private fun BackgroundBanner(
    @DrawableRes avatarRes: Int,
    modifier: Modifier = Modifier,
    res: Resources = LocalContext.current.resources
) {
    val brush = remember(key1 = avatarRes) {
        ShaderBrush(
            BitmapShader(
                ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(),
                Shader.TileMode.REPEAT,
                Shader.TileMode.REPEAT
            )
        )
    }

    Box(
        modifier = modifier.background(brush)
    ) {
        /* ... */
    }
}

در قطعه کد بعدی، state به یک کلاس MyAppState که یک کلاس ساده برای نگهداری state است، منتقل می‌شود. این کار یک تابع rememberMyAppState را برای مقداردهی اولیه یک نمونه از کلاس با استفاده remember در معرض نمایش قرار می‌دهد. نمایش چنین توابعی برای ایجاد نمونه‌ای که از ترکیب‌های مجدد جان سالم به در می‌برد، یک الگوی رایج در Compose است. تابع rememberMyAppState windowSizeClass دریافت می‌کند که به عنوان پارامتر key برای remember عمل می‌کند. اگر این پارامتر تغییر کند، برنامه باید کلاس ساده برای نگهداری state را با آخرین مقدار دوباره ایجاد کند. این اتفاق ممکن است در صورتی رخ دهد که، برای مثال، کاربر دستگاه را بچرخاند.

@Composable
private fun rememberMyAppState(
    windowSizeClass: WindowSizeClass
): MyAppState {
    return remember(windowSizeClass) {
        MyAppState(windowSizeClass)
    }
}

@Stable
class MyAppState(
    private val windowSizeClass: WindowSizeClass
) { /* ... */ }

Compose از پیاده‌سازی تساوی کلاس برای تصمیم‌گیری در مورد تغییر یک کلید و نامعتبر کردن مقدار ذخیره شده استفاده می‌کند.

ذخیره حالت با کلیدهایی فراتر از ترکیب مجدد

رابط برنامه‌نویسی کاربردی rememberSaveable یک پوشش پیرامون remember است که می‌تواند داده‌ها را در یک Bundle ذخیره کند. این رابط برنامه‌نویسی کاربردی به state اجازه می‌دهد نه تنها در ترکیب مجدد، بلکه در بازآفرینی فعالیت و مرگ فرآیند آغاز شده توسط سیستم نیز زنده بماند. rememberSaveable پارامترهای input را به همان منظوری دریافت می‌کند که remember keys دریافت می‌کند. حافظه پنهان (cache) با تغییر هر یک از ورودی‌ها نامعتبر می‌شود . دفعه بعد که تابع ترکیب مجدد می‌شود، rememberSaveable بلوک لامبدا محاسباتی را دوباره اجرا می‌کند.

در مثال زیر، rememberSaveable تا زمانی که typedQuery تغییر نکند، userTypedQuery ذخیره می‌کند:

var userTypedQuery by rememberSaveable(typedQuery, stateSaver = TextFieldValue.Saver) {
    mutableStateOf(
        TextFieldValue(text = typedQuery, selection = TextRange(typedQuery.length))
    )
}

بیشتر بدانید

برای کسب اطلاعات بیشتر در مورد state و Jetpack Compose، به منابع اضافی زیر مراجعه کنید.

نمونه‌ها

کدلبز

ویدیوها

وبلاگ‌ها

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