در Compose، رابط کاربری immutable است؛ یعنی پس از اینکه رسم شد، راهی برای بهروزرسانی مستقیم آن وجود ندارد. چیزی که میتوانید کنترل کنید، State مربوط به UI است. هر بار که State رابط کاربری تغییر کند، Compose بخشهایی از درخت UI را که تغییر کردهاند دوباره ایجاد میکند.
Composableها میتوانند State دریافت کنند و Eventها را در معرض استفاده قرار دهند. برای مثال، یک TextField مقدار value را دریافت میکند و یک callback به نام onValueChange ارائه میدهد که از handler مربوط به callback درخواست میکند مقدار را تغییر دهد.
var name by remember { mutableStateOf("") }
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Name") }
)
از آنجا که Composableها State را دریافت میکنند و Eventها را ارائه میدهند، الگوی جریان داده یکطرفه یا Unidirectional Data Flow بهخوبی با Jetpack Compose سازگار است.
این راهنما روی موارد زیر تمرکز دارد:
- نحوه پیادهسازی الگوی جریان داده یکطرفه در Compose
- نحوه پیادهسازی Eventها و State holderها
- نحوه کار با ViewModelها در Compose
نکته:
لایههای دیگر اپلیکیشن شما، یعنی Data layer و Business layer، با استفاده از Jetpack Compose تحت تأثیر قرار نمیگیرند. برای یادگیری بیشتر درباره معماری همه لایههای اپلیکیشن، راهنمای معماری اپلیکیشن را ببینید.
جریان داده یکطرفه
جریان داده یکطرفه یا UDF یک الگوی طراحی است که در آن State به سمت پایین جریان پیدا میکند و Eventها به سمت بالا جریان پیدا میکنند.
با دنبال کردن جریان داده یکطرفه، میتوانید Composableهایی را که State را در UI نمایش میدهند از بخشهایی از اپلیکیشن که State را ذخیره و تغییر میدهند جدا کنید.
حلقه بهروزرسانی UI برای اپلیکیشنی که از جریان داده یکطرفه استفاده میکند به این شکل است:
Event:
بخشی از UI یک Event تولید میکند و آن را به سمت بالا ارسال میکند؛ برای مثال، کلیک روی یک دکمه به ViewModel ارسال میشود تا مدیریت شود. همچنین ممکن است Event از لایههای دیگر اپلیکیشن ارسال شود؛ مثلاً اعلام شود که session کاربر منقضی شده است.
Update state:
یک Event handler ممکن است State را تغییر دهد.
Display state:
State holder، State را به سمت پایین ارسال میکند و UI آن را نمایش میدهد.
Eventها از UI به سمت State holder بالا میروند، و State از State holder به سمت UI پایین میآید.
دنبال کردن این الگو هنگام استفاده از Jetpack Compose چند مزیت دارد:
Testability:
جدا کردن State از UIای که آن را نمایش میدهد، تست کردن هرکدام را بهصورت جداگانه آسانتر میکند.
State encapsulation:
چون State فقط در یک مکان بهروزرسانی میشود و فقط یک منبع حقیقت برای State یک Composable وجود دارد، احتمال ایجاد bug به دلیل Stateهای ناسازگار کمتر میشود.
UI consistency:
همه بهروزرسانیهای State بلافاصله در UI منعکس میشوند؛ با استفاده از State holderهای قابل مشاهده مانند StateFlow یا LiveData.
جریان داده یکطرفه در Jetpack Compose
Composableها بر اساس State و Event کار میکنند. برای مثال، یک TextField فقط زمانی بهروزرسانی میشود که پارامتر value آن بهروزرسانی شود و یک callback به نام onValueChange ارائه میدهد؛ این callback یک Event است که درخواست میکند مقدار به مقدار جدیدی تغییر کند.
Compose، objectای به نام State را بهعنوان نگهدارنده مقدار تعریف میکند و تغییر مقدار State باعث Recomposition میشود.
میتوانید State را داخل یکی از موارد زیر نگه دارید، بسته به اینکه لازم است مقدار تا چه مدت remember شود:
remember { mutableStateOf(value) }
یا:
rememberSaveable { mutableStateOf(value) }
نوع value مربوط به Composable TextField، نوع String است؛ بنابراین این مقدار میتواند از هر جایی بیاید: از یک مقدار hardcoded، از یک ViewModel، یا از Composable والد.
لازم نیست حتماً آن را داخل یک object از نوع State نگه دارید، اما لازم است وقتی onValueChange فراخوانی میشود، مقدار را بهروزرسانی کنید.
نکته کلیدی:
mutableStateOf(value) یک MutableState ایجاد میکند که یک نوع observable در Compose است. هر تغییری در مقدار آن باعث زمانبندی Recomposition برای همه توابع Composableای میشود که آن مقدار را خواندهاند.
remember objectها را داخل Composition ذخیره میکند و وقتی Composableای که remember را فراخوانی کرده از Composition حذف شود، آن object را فراموش میکند.
rememberSaveable با ذخیره State داخل یک Bundle، State را در تغییرات Configuration حفظ میکند.
نکته:
برای یادگیری بیشتر درباره State و State hoisting در Compose، بخش State and Jetpack Compose را ببینید.
تعریف پارامترهای Composable
هنگام تعریف پارامترهای State برای یک Composable، این پرسشها را در نظر داشته باشید:
- این Composable چقدر قابل استفاده مجدد یا انعطافپذیر است؟
- پارامترهای State چه تأثیری روی عملکرد این Composable دارند؟
برای تقویت جداسازی و استفاده مجدد، هر Composable باید کمترین مقدار اطلاعات ممکن را نگه دارد.
برای مثال، هنگام ساخت یک Composable برای نمایش header یک مقاله خبری، بهتر است فقط اطلاعاتی را ارسال کنید که باید نمایش داده شوند، نه کل مقاله خبری را:
@Composable
fun Header(title: String, subtitle: String) {
// زمانی Recomposition میشود که title یا subtitle تغییر کرده باشند.
}
@Composable
fun Header(news: News) {
// زمانی Recomposition میشود که یک instance جدید از News ارسال شود.
}
گاهی استفاده از پارامترهای جداگانه عملکرد را نیز بهتر میکند. برای مثال، اگر News اطلاعات بیشتری بهجز title و subtitle داشته باشد، هر زمان یک instance جدید از News به Header(news) ارسال شود، Composable دوباره Recomposition میشود؛ حتی اگر title و subtitle تغییر نکرده باشند.
تعداد پارامترهایی را که ارسال میکنید با دقت در نظر بگیرید. داشتن تابعی با تعداد زیاد پارامتر، استفاده از آن را سختتر و کمراحتتر میکند. در چنین حالتی، بهتر است آنها را در یک کلاس گروهبندی کنید.
Eventها در Compose
هر ورودی به اپلیکیشن شما باید بهعنوان یک Event نمایش داده شود: لمسها، تغییرات متن، timerها یا حتی بهروزرسانیهای دیگر.
از آنجا که این Eventها State مربوط به UI را تغییر میدهند، ViewModel باید آنها را مدیریت کند و UI State را بهروزرسانی کند.
لایه UI هرگز نباید خارج از یک Event handler، State را تغییر دهد؛ چون این کار میتواند ناسازگاری و bug در اپلیکیشن ایجاد کند.
بهتر است برای State و lambdaهای مربوط به Event handler، مقدارهای immutable ارسال کنید. این رویکرد مزایای زیر را دارد:
- قابلیت استفاده مجدد را بهتر میکنید.
- مطمئن میشوید UI مقدار State را مستقیماً تغییر نمیدهد.
- از مشکلات concurrency جلوگیری میکنید، چون مطمئن میشوید State از thread دیگری تغییر داده نمیشود.
- اغلب پیچیدگی کد را کاهش میدهید.
برای مثال، یک Composable که یک String و یک lambda را بهعنوان پارامتر دریافت میکند، میتواند در contextهای مختلف فراخوانی شود و بسیار قابل استفاده مجدد است.
فرض کنید top app bar در اپلیکیشن شما همیشه متن نمایش میدهد و یک دکمه back دارد. میتوانید یک Composable عمومیتر به نام MyAppTopAppBar تعریف کنید که متن و handler دکمه back را بهعنوان پارامتر دریافت میکند:
@Composable
fun MyAppTopAppBar(topAppBarText: String, onBackPressed: () -> Unit) {
TopAppBar(
title = {
Text(
text = topAppBarText,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxSize()
.wrapContentSize(Alignment.Center)
)
},
navigationIcon = {
IconButton(onClick = onBackPressed) {
Icon(
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = localizedString
)
}
},
// ...
)
}
ViewModelها، Stateها و Eventها: یک مثال
با استفاده از ViewModel و mutableStateOf میتوانید جریان داده یکطرفه را در اپلیکیشن خود معرفی کنید، اگر یکی از موارد زیر درست باشد:
- State مربوط به UI شما با استفاده از State holderهای قابل مشاهده مانند
StateFlowیاLiveDataارائه میشود. - ViewModel، Eventهایی را که از UI یا لایههای دیگر اپلیکیشن میآیند مدیریت میکند و بر اساس آن Eventها، State holder را بهروزرسانی میکند.
برای مثال، هنگام پیادهسازی صفحه sign-in، لمس دکمه Sign in باید باعث شود اپلیکیشن یک progress spinner نمایش دهد و یک درخواست شبکه اجرا کند.
اگر login موفق بود، اپلیکیشن به صفحه دیگری navigate میکند. اگر خطایی رخ داد، اپلیکیشن یک Snackbar نمایش میدهد.
در ادامه میبینید که چگونه میتوان State صفحه و Event را مدلسازی کرد.
صفحه چهار State دارد:
Signed out:
وقتی کاربر هنوز وارد نشده است.
In progress:
وقتی اپلیکیشن در حال تلاش برای ورود کاربر است و یک درخواست شبکه انجام میدهد.
Error:
وقتی هنگام ورود خطایی رخ داده است.
Signed in:
وقتی کاربر وارد شده است.
میتوانید این Stateها را بهصورت یک sealed class مدلسازی کنید. ViewModel، State را بهصورت یک State ارائه میدهد، State اولیه را تنظیم میکند و در صورت نیاز State را بهروزرسانی میکند. همچنین ViewModel با ارائه متد onSignIn()، Event مربوط به sign-in را مدیریت میکند.
class MyViewModel : ViewModel() {
private val _uiState = mutableStateOf<UiState>(UiState.SignedOut)
val uiState: State<UiState>
get() = _uiState
// ...
}
mutableStateOf، Compose برای LiveData، Flow و Observable نیز extensionهایی فراهم میکند تا بهعنوان listener ثبت شوند و مقدار را بهصورت State نمایش دهند.class MyViewModel : ViewModel() {
private val _uiState = MutableLiveData<UiState>(UiState.SignedOut)
val uiState: LiveData<UiState>
get() = _uiState
// ...
}
@Composable
fun MyComposable(viewModel: MyViewModel) {
val uiState = viewModel.uiState.observeAsState()
// ...
}
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: منبع مستندات