State و Jetpack Compose

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

همه برنامه های اندروید وضعیت را به کاربر نمایش می دهند. چند نمونه از حالت در برنامه های اندروید:

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

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

حالت و ترکیب

Compose اعلانی است و به این ترتیب تنها راه برای به روز رسانی آن فراخوانی همان composable با آرگومان های جدید است. این آرگومان ها نمایشی از وضعیت UI هستند. هر زمان که وضعیتی به روز می شود ، ترکیب مجدد صورت می گیرد. در نتیجه، چیزهایی مانند 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 می توانند از remember API برای ذخیره یک شی در حافظه استفاده کنند. مقداری که با remember محاسبه می شود در ترکیب اولیه در Composition ذخیره می شود و مقدار ذخیره شده در طول ترکیب مجدد برگردانده می شود. remember می تواند برای ذخیره اشیاء تغییرپذیر و غیرقابل تغییر استفاده شود.

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

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

این اعلان ها معادل هستند و به عنوان قند نحوی برای کاربردهای مختلف حالت ارائه می شوند. شما باید کدی را انتخاب کنید که راحت‌ترین کد را در فایلی که می‌نویسید تولید کند.

دستور by delegate به واردات زیر نیاز دارد:

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

می‌توانید از مقدار به خاطر سپرده‌شده به‌عنوان پارامتری برای ترکیب‌پذیری‌های دیگر یا حتی به‌عنوان منطق در عبارات برای تغییر اینکه کدام ترکیب‌پذیر نمایش داده می‌شوند، استفاده کنید. به عنوان مثال، اگر نمی خواهید پیام تبریک را در صورت خالی بودن نام نمایش دهید، از حالت در عبارت 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 به شما کمک می‌کند حالت را در بین ترکیب‌های مجدد حفظ کنید، وضعیت در تغییرات پیکربندی حفظ نمی‌شود. برای این کار باید از rememberSaveable استفاده کنید. rememberSaveable به طور خودکار هر مقداری را که می توان در یک Bundle ذخیره کرد ذخیره می کند. برای مقادیر دیگر، می‌توانید یک شی محافظ سفارشی ارسال کنید.

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

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

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

  • Flow : collectAsStateWithLifecycle()

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

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

کاتلین

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

شیار

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

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

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

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

  • LiveData : observeAsState()

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

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

کاتلین

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

شیار

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

کاتلین

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

شیار

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

کاتلین

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

شیار

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

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

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

ترکیب‌پذیر بدون حالت ، ترکیب‌پذیری است که هیچ حالتی ندارد. یک راه آسان برای دستیابی به حالت بدون تابعیت استفاده از بالابرهای دولتی است.

همانطور که شما قابلیت‌های قابل استفاده مجدد را توسعه می‌دهید، اغلب می‌خواهید هم یک نسخه حالت دار و هم یک نسخه بدون حالت از یک قابلیت ترکیب‌سازی را در معرض نمایش قرار دهید. نسخه Stateful برای تماس‌گیرندگانی که به وضعیت اهمیتی نمی‌دهند مناسب است و نسخه بدون حالت برای تماس‌گیرندگانی که نیاز به کنترل یا بالا بردن وضعیت دارند، ضروری است.

بالابر دولتی

بالا بردن حالت در Compose الگویی از حرکت حالت به فراخوان کننده یک Composable برای ایجاد حالت Composable بدون حالت است. الگوی کلی برای بالا بردن حالت در Jetpack Compose جایگزینی متغیر حالت با دو پارامتر است:

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

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

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

  • منبع منفرد حقیقت: با جابجایی حالت به جای تکرار آن، اطمینان حاصل می کنیم که تنها یک منبع حقیقت وجود دارد. این به جلوگیری از اشکالات کمک می کند.
  • کپسوله شده: فقط ترکیب‌پذیرهای حالت دار می‌توانند حالت خود را تغییر دهند. کاملا داخلیه
  • قابل اشتراک‌گذاری: حالت Hoisted را می‌توان با چندین ترکیب قابل اشتراک‌گذاری به اشتراک گذاشت. اگر می‌خواهید name با یک ترکیب متفاوت بخوانید، بالا بردن این امکان را به شما می‌دهد.
  • قابل رهگیری: تماس گیرندگان با قابلیت های ترکیبی بدون حالت می توانند تصمیم بگیرند که رویدادها را قبل از تغییر حالت نادیده بگیرند یا تغییر دهند.
  • Decoupled: حالت برای ترکیبات بدون حالت ممکن است در هر جایی ذخیره شود. برای مثال، اکنون امکان انتقال 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 ، استدلال در مورد ترکیب‌پذیر، استفاده مجدد از آن در موقعیت‌های مختلف و آزمایش آسان‌تر است. HelloContent از نحوه ذخیره وضعیت آن جدا شده است. جداسازی به این معنی است که اگر HelloScreen تغییر دهید یا جایگزین کنید، لازم نیست نحوه اجرای HelloContent را تغییر دهید.

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

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

در حال بازیابی حالت در Compose

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

راه های ذخیره سازی حالت

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

بسته بندی کنید

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

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

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

MapSaver

اگر به دلایلی @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

برای جلوگیری از نیاز به تعریف کلیدها برای نقشه، می توانید 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

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

برای کسب اطلاعات بیشتر، بالا بردن حالت را در مستندات Compose یا به طور کلی‌تر، صفحه دارندگان State و UI State را در راهنمای معماری ببینید.

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

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

var name by remember { mutableStateOf("") }

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

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

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

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

remember مقدار را تا زمانی که از Composition خارج شود ذخیره می کند. با این حال، راهی برای باطل کردن مقدار ذخیره شده وجود دارد. remember API نیز یک پارامتر key یا keys را می گیرد. اگر هر یک از این کلیدها تغییر کرد، دفعه بعد که تابع دوباره ترکیب شد ، remember که حافظه پنهان را باطل می کند و بلوک لامبدا محاسبه را دوباره اجرا می کند . این مکانیسم به شما امکان کنترل طول عمر یک شی در 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 بالا می رود. این یک تابع rememberMyAppState را برای مقداردهی اولیه یک نمونه از کلاس با استفاده از remember نمایش می دهد. نمایش چنین توابعی برای ایجاد نمونه ای که از ترکیب مجدد جان سالم به در می برد، یک الگوی رایج در Compose است. تابع rememberMyAppState windowSizeClass دریافت می کند که به عنوان پارامتر key برای remember عمل می کند. اگر این پارامتر تغییر کند، برنامه باید کلاس دارنده حالت ساده را با آخرین مقدار دوباره ایجاد کند. برای مثال، اگر کاربر دستگاه را بچرخاند، ممکن است این اتفاق بیفتد.

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

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

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

وضعیت ذخیره با کلیدهای فراتر از ترکیب مجدد

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

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

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

بیشتر بدانید

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

نمونه ها

Codelabs

ویدیوها

وبلاگ ها

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