حالت و Jetpack Compose

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

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

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

Jetpack Compose به شما کمک می‌کند تا به‌طور صریح مشخص کنید که کجا و چگونه حالت را در برنامه Android ذخیره و استفاده کنید. این راهنما بر ارتباط بین حالت و composableها و میاناهای برنامه‌سازی کاربردی که Jetpack Compose برای کار با حالت ارائه می‌دهد تمرکز دارد.

وضعیت و ترکیب‌بندی

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

ترکیب‌بندی اولیه: ایجاد «ترکیب‌بندی» با اجرای عناصر ترکیبی برای اولین‌بار.

ترکیب‌بندی مجدد: اجرای مجدد عناصر قابل‌ترکیب برای به‌روزرسانی «ترکیب‌بندی» هنگام تغییر داده‌ها.

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

حالت در composable

توابع ترکیب‌پذیر می‌توانند از remember API برای ذخیره کردن شیء در حافظه استفاده کنند. مقداری که توسط 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) }

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

نحو نماینده by به واردات زیر نیاز دارد:

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 ذخیره شود ذخیره می‌کند. برای مقادیر دیگر، می‌توانید یک شیء ذخیره‌کننده سفارشی را ارسال کنید.

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

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

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

  • Flow: collectAsStateWithLifecycle()

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

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

کاتلین

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

شیک

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

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

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

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

  • LiveData: observeAsState()

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

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

کاتلین

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

شیک

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

کاتلین

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

شیک

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

کاتلین

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

شیک

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

حفظ سابقه درمقابل عدم حفظ سابقه

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

یک ترکیب‌پذیر بدون حالت ترکیب‌پذیری است که هیچ حالتی را نگه نمی‌دارد. یک روش رایج برای دستیابی به بی‌حالت بودن، استفاده از بالابردن حالت است.

As you develop reusable composables, you often want to expose both a stateful and a stateless version of the same composable. نسخه باحالت برای تماس‌گیرندگانی که به حالت اهمیت نمی‌دهند مناسب است، و نسخه بی‌حالت برای تماس‌گیرندگانی که نیاز به کنترل یا ارتقای حالت دارند ضروری است.

بالابردن حالت

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

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

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

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

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

جریان داده یک‌سویه

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

  1. وضعیت باید حداقل به پایین‌ترین والد مشترک همه ترکیب‌پذیرهایی که از وضعیت استفاده می‌کنند (می‌خوانند) ارتقا یابد.
  2. وضعیت باید حداقل به بالاترین سطحی که ممکن است تغییر کند (نوشتن) ارتقا یابد.
  3. اگر دو حالت در پاسخ به رویدادهای یکسان تغییر کنند، باید باهم بالا برده شوند.

می‌توانید وضعیت را بالاتر از آنچه این قوانین نیاز دارند بالا ببرید، اما پایین بردن وضعیت پیروی از جریان داده یک‌سویه را دشوار یا غیرممکن می‌کند.

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

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

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

روش‌های ذخیره کردن وضعیت

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

Parcelize

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

@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 یا به‌طور کلی‌تر به صفحه نگه‌دارنده‌های حالت و حالت میانای کاربر در راهنمای معماری مراجعه کنید.

وقتی کلیدها تغییر می‌کنند، محاسبات به‌خاطر سپردن را دوباره راه‌اندازی کنید

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

var name by remember { mutableStateOf("") }

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

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

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

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

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

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

در این قطعه کد، یک ShaderBrush ایجاد شده و به‌عنوان رنگ پس‌زمینه یک Box ترکیب‌پذیر استفاده می‌شود. 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)
    ) {
        /* ... */
    }
}

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

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

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

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

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

بیشتر بدانید

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

نمونه‌ها

Codelabs

ویدیوها

وبلاگ‌ها