State در یک اپلیکیشن، هر مقداری است که میتواند در طول زمان تغییر کند. این تعریف بسیار گسترده است و از یک پایگاه داده Room گرفته تا یک متغیر در یک کلاس را شامل میشود.
همه اپلیکیشنهای اندروید State را به کاربر نمایش میدهند. چند مثال از State در اپلیکیشنهای اندروید:
Jetpack Compose به شما کمک میکند دقیق و واضح مشخص کنید که State را در کجا و چگونه در یک اپلیکیشن اندروید ذخیره و استفاده میکنید. این راهنما روی ارتباط بین State و Composableها و همچنین APIهایی تمرکز دارد که Jetpack Compose برای کار آسانتر با State ارائه میدهد.
Compose اعلانی است؛ بنابراین تنها راه بهروزرسانی آن این است که همان Composable را با آرگومانهای جدید دوباره فراخوانی کنید. این آرگومانها نماینده وضعیت رابط کاربری یا UI State هستند. هر بار که یک State بهروزرسانی میشود، Recomposition رخ میدهد.
در نتیجه، چیزهایی مانند TextField بهصورت خودکار مثل Viewهای قدیمی مبتنی بر XML بهروزرسانی نمیشوند. یک Composable باید بهطور صریح State جدید را دریافت کند تا مطابق آن بهروزرسانی شود.
@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 آن تغییر کند. این موضوع به نحوه کار Composition و Recomposition در Compose مربوط است.
اصطلاح کلیدی:
Composition: توصیفی از رابط کاربری است که Jetpack Compose هنگام اجرای Composableها میسازد.
Initial composition: ایجاد یک Composition با اجرای Composableها برای اولین بار.
Recomposition: اجرای دوباره Composableها برای بهروزرسانی Composition هنگام تغییر دادهها.
برای یادگیری بیشتر درباره Initial Composition و Recomposition، بخش Thinking in Compose را ببینید.
توابع Composable میتوانند از API به نام remember برای ذخیره یک شیء در حافظه استفاده کنند. مقداری که توسط remember محاسبه میشود، در مرحله Initial Composition داخل Composition ذخیره میشود و در Recompositionهای بعدی همان مقدار ذخیرهشده برگردانده میشود.
از remember میتوان برای ذخیره اشیای mutable و immutable استفاده کرد.
نکته:remember اشیا را در Composition ذخیره میکند و وقتی Composableای که remember را فراخوانی کرده از Composition حذف شود، آن شیء را فراموش میکند.
mutableStateOf یک MutableState<T> قابل مشاهده ایجاد میکند؛ این نوع قابل مشاهده با Runtime مربوط به Compose یکپارچه است.
interface MutableState<T> : State<T> {
override var value: T
}
هر تغییری در value باعث زمانبندی Recomposition برای همه توابع Composableای میشود که آن مقدار را خواندهاند.
سه روش برای تعریف یک شیء 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های زیر نیاز دارد:
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 به شما کمک میکند State را در Recompositionها حفظ کنید، این State در تغییرات configuration حفظ نمیشود. برای این کار باید از rememberSaveable استفاده کنید.
rememberSaveable بهصورت خودکار هر مقداری را که بتوان در یک Bundle ذخیره کرد، ذخیره میکند. برای مقادیر دیگر میتوانید یک شیء saver سفارشی ارائه دهید.
احتیاط:
استفاده از اشیای mutable مانند ArrayList<T> یا mutableListOf() بهعنوان State در Compose باعث میشود کاربران دادههای اشتباه یا قدیمی در اپلیکیشن ببینند. اشیای mutable که قابل مشاهده نیستند، مانند ArrayList<T> یا یک data class قابل تغییر، توسط Compose مشاهده نمیشوند و هنگام تغییر باعث Recomposition نمیشوند.
بهجای استفاده از اشیای mutable غیرقابل مشاهده، پیشنهاد میشود از یک نگهدارنده داده قابل مشاهده مانند State<List<T>> و لیست immutable مثل listOf() استفاده کنید.
Compose شما را مجبور نمیکند که حتماً از MutableState<T> برای نگهداری State استفاده کنید؛ بلکه از انواع قابل مشاهده دیگر نیز پشتیبانی میکند. قبل از خواندن یک نوع observable دیگر در Compose، باید آن را به State<T> تبدیل کنید تا Composableها بتوانند هنگام تغییر State بهصورت خودکار Recomposition شوند.
Compose توابعی برای ساخت State<T> از انواع observable رایج در اپلیکیشنهای اندروید ارائه میدهد. قبل از استفاده از این یکپارچهسازیها، artifactهای مناسب را اضافه کنید.
تابع collectAsStateWithLifecycle() مقادیر یک Flow را به روشی آگاه از چرخه عمر جمعآوری میکند و به اپلیکیشن کمک میکند منابع خود را حفظ کند. این تابع آخرین مقدار emit شده را بهعنوان Compose State نمایش میدهد.
از این API بهعنوان روش پیشنهادی برای collect کردن Flowها در اپلیکیشنهای اندروید استفاده کنید.
وابستگی زیر در فایل build.gradle لازم است:
dependencies {
...
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.10.0")
}
تابع collectAsState مشابه collectAsStateWithLifecycle است، چون آن هم مقادیر یک Flow را جمعآوری کرده و آن را به Compose State تبدیل میکند.
برای کدهای مستقل از پلتفرم از collectAsState استفاده کنید، چون collectAsStateWithLifecycle فقط مخصوص اندروید است.
برای collectAsState وابستگی اضافی لازم نیست، چون در compose-runtime موجود است.
تابع observeAsState() شروع به مشاهده این LiveData میکند و مقادیر آن را از طریق State نمایش میدهد.
وابستگی زیر در فایل build.gradle لازم است:
dependencies {
...
implementation("androidx.compose.runtime:runtime-livedata:1.11.3")
}
توابع subscribeAsState() توابع extension هستند که streamهای reactive مربوط به RxJava2، مانند Single، Observable و Completable را به Compose State تبدیل میکنند.
وابستگی زیر در فایل build.gradle لازم است:
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava2:1.11.3")
}
توابع subscribeAsState() توابع extension هستند که streamهای reactive مربوط به RxJava3، مانند Single، Observable و Completable را به Compose State تبدیل میکنند.
وابستگی زیر در فایل build.gradle لازم است:
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava3:1.11.3")
}
نکته کلیدی:
Compose هنگام خواندن objectهای State بهصورت خودکار Recomposition انجام میدهد. اگر در Compose از نوع observable دیگری مانند LiveData استفاده میکنید، باید قبل از خواندن آن، آن را به State تبدیل کنید. مطمئن شوید این تبدیل نوع داخل یک Composable و با استفاده از یک extension function مانند LiveData<T>.observeAsState() انجام میشود.
نکته:
شما به این یکپارچهسازیها محدود نیستید. میتوانید برای Jetpack Compose یک extension function بسازید که انواع observable دیگر را بخواند. اگر اپلیکیشن شما از یک کلاس observable سفارشی استفاده میکند، آن را با استفاده از API به نام produceState به State<T> تبدیل کنید.
هر شیئی که به Jetpack Compose اجازه دهد در هر تغییر مشترک شود، میتواند به State<T> تبدیل شده و داخل یک Composable خوانده شود.
یک Composable که از remember برای ذخیره یک شیء استفاده میکند، State داخلی ایجاد میکند و بنابراین آن Composable، Stateful میشود.
HelloContent نمونهای از یک Composable دارای State است، چون State مربوط به name را بهصورت داخلی نگه میدارد و تغییر میدهد. این موضوع میتواند در موقعیتهایی مفید باشد که caller نیازی به کنترل State ندارد و میتواند بدون مدیریت State از آن استفاده کند.
با این حال، Composableهایی که State داخلی دارند معمولاً کمتر قابل استفاده مجدد هستند و تست کردن آنها سختتر است.
یک Composable بدون State یا Stateless، Composableای است که هیچ Stateای را نگه نمیدارد. یک راه ساده برای رسیدن به Stateless بودن، استفاده از State hoisting است.
وقتی Composableهای قابل استفاده مجدد توسعه میدهید، معمولاً میخواهید هم نسخه Stateful و هم نسخه Stateless همان Composable را ارائه دهید. نسخه Stateful برای callerهایی مناسب است که به State اهمیت نمیدهند، و نسخه Stateless برای callerهایی ضروری است که نیاز دارند State را کنترل یا Hoist کنند.
State hoisting در Compose الگویی است که در آن State به caller مربوط به Composable منتقل میشود تا Composable بدون State شود.
الگوی کلی State hoisting در Jetpack Compose این است که متغیر State را با دو پارامتر جایگزین کنید:
value: T
مقدار فعلی که باید نمایش داده شود.
onValueChange: (T) -> Unit
رویدادی که درخواست تغییر مقدار را میدهد؛ در اینجا T مقدار جدید پیشنهادی است.
البته شما محدود به onValueChange نیستید. اگر رویدادهای مشخصتری برای Composable مناسب هستند، باید آنها را با استفاده از lambdaها تعریف کنید.
Stateای که به این روش Hoist میشود چند ویژگی مهم دارد:
Single source of truth:
با منتقل کردن State بهجای کپی کردن آن، مطمئن میشویم که فقط یک منبع حقیقت وجود دارد. این کار به جلوگیری از باگها کمک میکند.
Encapsulated:
فقط Composableهای دارای State میتوانند State خودشان را تغییر دهند. این State کاملاً داخلی است.
Shareable:
State Hoist شده میتواند با چند Composable به اشتراک گذاشته شود. اگر بخواهید name را در یک Composable دیگر بخوانید، Hoisting این امکان را فراهم میکند.
Interceptable:
Callerهای Composableهای Stateless میتوانند قبل از تغییر State، رویدادها را نادیده بگیرند یا تغییر دهند.
Decoupled:
State مربوط به Composableهای Stateless میتواند در هر جایی ذخیره شود. برای مثال، اکنون میتوان name را به ViewModel منتقل کرد.
در مثال زیر، name و onValueChange از HelloContent خارج شده و در درخت به سمت بالا به یک Composable به نام 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") })
}
}
با خارج کردن State از HelloContent، درک Composable، استفاده مجدد از آن در شرایط مختلف و تست کردن آن آسانتر میشود. HelloContent از نحوه ذخیره شدن State خودش جدا میشود. این جداشدگی یعنی اگر HelloScreen را تغییر دهید یا جایگزین کنید، لازم نیست نحوه پیادهسازی HelloContent را تغییر دهید.
الگویی که در آن State به پایین میرود و رویدادها به بالا میآیند، جریان داده یکطرفه یا Unidirectional Data Flow نام دارد.
در این مثال، State از HelloScreen به HelloContent پایین میرود و رویدادها از HelloContent به HelloScreen بالا میآیند. با دنبال کردن جریان داده یکطرفه، میتوانید Composableهایی را که State را در UI نمایش میدهند، از بخشهایی از اپلیکیشن که State را ذخیره و تغییر میدهند جدا کنید.
نکته کلیدی:
هنگام Hoist کردن State، سه قانون وجود دارد که به شما کمک میکند بفهمید State باید کجا قرار بگیرد:
میتوانید State را بالاتر از چیزی که این قوانین لازم میدانند Hoist کنید، اما کم Hoist کردن State باعث میشود دنبال کردن جریان داده یکطرفه سخت یا غیرممکن شود.
API به نام rememberSaveable مشابه remember عمل میکند، چون State را در Recompositionها حفظ میکند. علاوه بر آن، State را در بازسازی Activity یا فرایند نیز با استفاده از مکانیزم saved instance state حفظ میکند. برای مثال، این اتفاق هنگام چرخاندن صفحه رخ میدهد.
نکته:rememberSaveable اگر Activity بهطور کامل توسط کاربر بسته شود، State را حفظ نمیکند. برای مثال، اگر کاربر Activity فعلی را از صفحه recent apps به بالا swipe کند، State حفظ نمیشود.
همه نوع دادههایی که به Bundle اضافه میشوند، بهصورت خودکار ذخیره میشوند. اگر بخواهید چیزی را ذخیره کنید که نمیتوان آن را به Bundle اضافه کرد، چند گزینه دارید.
سادهترین راه این است که annotation به نام @Parcelize را به object اضافه کنید. در این صورت object تبدیل به parcelable میشود و میتوان آن را در Bundle قرار داد.
برای مثال، کد زیر یک نوع داده 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 استفاده کنید تا قانون سفارشی خودتان را برای تبدیل یک object به مجموعهای از مقادیر قابل ذخیره در 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"))
}
}
برای اینکه نیازی به تعریف key برای map نداشته باشید، میتوانید از listSaver استفاده کنید و indexهای آن را بهعنوان key در نظر بگیرید.
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"))
}
}
State hoisting ساده را میتوان داخل خود توابع Composable مدیریت کرد. با این حال، اگر مقدار Stateهایی که باید دنبال شوند زیاد شود یا منطق مربوط به توابع Composable افزایش پیدا کند، بهتر است مسئولیت منطق و State را به کلاسهای دیگر بسپارید: State holderها.
اصطلاح کلیدی:
State holderها منطق و State مربوط به Composableها را مدیریت میکنند.
توجه داشته باشید که در منابع دیگر، State holderها با نام Hoisted state objects نیز شناخته میشوند.
برای یادگیری بیشتر، مستندات State hoisting در Compose یا بهطور کلی صفحه State holders and UI State در راهنمای معماری را ببینید.
API به نام remember اغلب همراه با MutableState استفاده میشود
var name by remember { mutableStateOf("") }
در اینجا، استفاده از تابع remember باعث میشود مقدار MutableState در Recompositionها باقی بماند.
بهطور کلی، remember یک پارامتر lambda برای محاسبه دریافت میکند. وقتی remember برای اولین بار اجرا میشود، lambda مربوط به محاسبه را اجرا کرده و نتیجه آن را ذخیره میکند. در Recomposition، remember همان مقداری را که آخرین بار ذخیره شده برمیگرداند.
علاوه بر cache کردن State، میتوانید از remember برای ذخیره هر object یا نتیجه عملیاتی در Composition استفاده کنید که مقداردهی اولیه یا محاسبه آن پرهزینه است. شاید نخواهید این محاسبه در هر Recomposition تکرار شود.
نمونه زیر ساختن یک object از نوع ShaderBrush را نشان میدهد که یک عملیات پرهزینه است:
val brush = remember {
ShaderBrush(
BitmapShader(
ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(),
Shader.TileMode.REPEAT,
Shader.TileMode.REPEAT
)
)
}
remember مقدار را تا زمانی که از Composition خارج شود ذخیره میکند. با این حال، راهی برای نامعتبر کردن مقدار cache شده وجود دارد.
API به نام remember میتواند یک یا چند پارامتر key نیز بگیرد. اگر هرکدام از این keyها تغییر کنند، دفعه بعد که تابع Recomposition شود، remember cache را نامعتبر میکند و بلوک lambda مربوط به محاسبه را دوباره اجرا میکند.
این مکانیزم به شما کنترل بیشتری روی طول عمر یک object در Composition میدهد. محاسبه تا زمانی معتبر باقی میماند که ورودیها تغییر نکرده باشند، نه فقط تا زمانی که مقدار remember شده از Composition خارج شود.
مثالهای زیر نشان میدهند این مکانیزم چگونه کار میکند.
در قطعهکد زیر، یک ShaderBrush ساخته شده و بهعنوان رنگ پسزمینه یک Composable از نوع Box استفاده میشود. remember نمونه ShaderBrush را ذخیره میکند، چون ساخت دوباره آن پرهزینه است. remember مقدار avatarRes را بهعنوان پارامتر key1 میگیرد که تصویر پسزمینه انتخابشده است.
اگر avatarRes تغییر کند، brush با تصویر جدید recomposition میشود و دوباره روی Box اعمال میشود. این حالت ممکن است زمانی رخ دهد که کاربر تصویر دیگری را از یک picker بهعنوان پسزمینه انتخاب کند.
@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 به یک کلاس State holder ساده به نام MyAppState منتقل شده است. این کلاس یک تابع rememberMyAppState ارائه میدهد تا با استفاده از remember یک نمونه از کلاس مقداردهی اولیه شود.
ارائه چنین توابعی برای ساختن نمونهای که در Recompositionها باقی بماند، یک الگوی رایج در Compose است.
تابع rememberMyAppState مقدار windowSizeClass را دریافت میکند که بهعنوان پارامتر key برای remember استفاده میشود. اگر این پارامتر تغییر کند، اپلیکیشن باید کلاس State holder ساده را با مقدار جدید دوباره بسازد. برای مثال، این اتفاق ممکن است زمانی رخ دهد که کاربر دستگاه را بچرخاند.
@Composable
private fun rememberMyAppState(
windowSizeClass: WindowSizeClass
): MyAppState {
return remember(windowSizeClass) {
MyAppState(windowSizeClass)
}
}
@Stable
class MyAppState(
private val windowSizeClass: WindowSizeClass
) { /* ... */ }
نکته:
برای اطلاعات بیشتر درباره کلاسهای State holder ساده، مستندات Plain state holder class as state owner یا بخش State holders and UI State در راهنمای Architecture را ببینید.
Compose از پیادهسازی equals مربوط به کلاس استفاده میکند تا تصمیم بگیرد آیا یک key تغییر کرده و مقدار ذخیرهشده باید نامعتبر شود یا نه.
نکته:
در نگاه اول، استفاده از remember همراه با keyها ممکن است شبیه استفاده از APIهای دیگر Compose مانند derivedStateOf به نظر برسد. برای یادگیری تفاوت آنها، مقاله Jetpack Compose — When should I use derivedStateOf? را ببینید.
API به نام rememberSaveable یک wrapper روی remember است که میتواند داده را در یک Bundle ذخیره کند. این API اجازه میدهد State نه تنها در Recomposition، بلکه در بازسازی Activity و حتی مرگ process توسط سیستم نیز باقی بماند.
rememberSaveable پارامترهای input را برای همان هدفی دریافت میکند که remember keyها را دریافت میکند. اگر هرکدام از inputها تغییر کنند، cache نامعتبر میشود. دفعه بعد که تابع Recomposition شود، rememberSaveable بلوک lambda مربوط به محاسبه را دوباره اجرا میکند.
نکته:
یک تفاوت در نامگذاری API وجود دارد که باید به آن توجه کنید. در API مربوط به remember از نام پارامتر keys استفاده میشود، اما در rememberSaveable برای همان هدف از inputs استفاده میشود. اگر هرکدام از این پارامترها تغییر کنند، مقدار cache شده نامعتبر میشود.
در مثال زیر، rememberSaveable مقدار userTypedQuery را تا زمانی که typedQuery تغییر کند ذخیره میکند:
var userTypedQuery by rememberSaveable(typedQuery, stateSaver = TextFieldValue.Saver) {
mutableStateOf(
TextFieldValue(text = typedQuery, selection = TextRange(typedQuery.length))
)
}
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: منبع مستندات