رابط های کاربری مدرن به ندرت ثابت هستند. هنگامی که کاربر با رابط کاربری تعامل می کند یا زمانی که برنامه نیاز به نمایش داده های جدید دارد، وضعیت رابط کاربری تغییر می کند.
این سند دستورالعملهایی را برای تولید و مدیریت حالت رابط کاربری تجویز میکند. در پایان آن باید:
- بدانید که از چه APIهایی باید برای ایجاد حالت UI استفاده کنید. این بستگی به ماهیت منابع تغییر حالت موجود در دارندگان حالت شما دارد که از اصول جریان داده های یک طرفه پیروی می کنند.
- بدانید که چگونه باید تولید حالت UI را در نظر بگیرید تا از منابع سیستم آگاه باشید.
- بدانید چگونه باید وضعیت رابط کاربری را برای مصرف توسط UI در معرض دید قرار دهید.
اساساً، تولید حالت، کاربرد تدریجی این تغییرات در حالت رابط کاربری است. دولت همیشه وجود دارد و در نتیجه رویدادها تغییر می کند. تفاوت بین رویدادها و حالت ها در جدول زیر خلاصه شده است:
رویدادها | ایالت |
---|---|
گذرا، غیرقابل پیش بینی و برای یک دوره محدود وجود دارد. | همیشه وجود دارد. |
نهاده های تولید دولتی | خروجی تولید دولتی |
محصول رابط کاربری یا منابع دیگر. | توسط UI مصرف می شود. |
یک یادگاری بزرگ که موارد فوق را خلاصه می کند ، حالت است. اتفاقات رخ می دهد . نمودار زیر به تجسم تغییرات به حالتی که رویدادها در یک جدول زمانی رخ می دهند کمک می کند. هر رویداد توسط دارنده حالت مناسب پردازش می شود و منجر به تغییر حالت می شود:
![رویدادها در مقابل ایالت](https://developer.android.com/static/images/topic/architecture/ui-layer/events-vs-state.png?authuser=0&hl=fa)
رویدادها می توانند از:
- کاربران : همانطور که با رابط کاربری برنامه تعامل دارند.
- سایر منابع تغییر وضعیت : API هایی که داده های برنامه را از رابط کاربری، دامنه یا لایه های داده مانند رویدادهای مهلت زمانی نوار اسنک، موارد استفاده یا مخازن ارائه می کنند.
خط لوله تولید دولتی UI
تولید دولتی در برنامه های اندروید را می توان به عنوان یک خط لوله پردازش در نظر گرفت که شامل موارد زیر است:
- ورودی ها : منابع تغییر حالت. آنها ممکن است:
- Local to layer UI: اینها می توانند رویدادهای کاربر مانند وارد کردن عنوانی برای یک "to-do" در یک برنامه مدیریت کار یا API هایی باشند که دسترسی به منطق UI را فراهم می کنند که باعث ایجاد تغییرات در وضعیت UI می شود. برای مثال، فراخوانی متد
open
درDrawerState
در Jetpack Compose. - خارجی به لایه UI: اینها منابعی از دامنه یا لایه های داده هستند که باعث تغییر در وضعیت UI می شوند. به عنوان مثال اخباری که بارگیری از یک
NewsRepository
یا رویدادهای دیگر به پایان رسید. - مخلوطی از تمام موارد بالا.
- Local to layer UI: اینها می توانند رویدادهای کاربر مانند وارد کردن عنوانی برای یک "to-do" در یک برنامه مدیریت کار یا API هایی باشند که دسترسی به منطق UI را فراهم می کنند که باعث ایجاد تغییرات در وضعیت UI می شود. برای مثال، فراخوانی متد
- دارندگان حالت : انواعی که منطق تجاری و/یا منطق رابط کاربری را در منابع تغییر حالت اعمال میکنند و رویدادهای کاربر را برای تولید وضعیت رابط کاربری پردازش میکنند.
- خروجی : حالت رابط کاربری که برنامه می تواند برای ارائه اطلاعات مورد نیاز کاربران ارائه دهد.
![خط لوله تولید دولتی](https://developer.android.com/static/images/topic/architecture/ui-layer/state-production-pipeline.png?authuser=0&hl=fa)
APIهای تولید دولتی
بسته به اینکه در چه مرحله ای از خط لوله هستید، دو API اصلی در تولید دولتی استفاده می شود:
مرحله خط لوله | API |
---|---|
ورودی | شما باید از APIهای ناهمزمان برای انجام کارهای خارج از رشته UI استفاده کنید تا jank UI را آزاد نگه دارید. به عنوان مثال، Coroutines یا Flow ها در Kotlin، و RxJava یا callbacks در زبان برنامه نویسی جاوا. |
خروجی | شما باید از APIهای دارنده داده قابل مشاهده برای باطل کردن و رندر کردن مجدد رابط کاربری در هنگام تغییر حالت استفاده کنید. برای مثال StateFlow، Compose State یا LiveData. دارندگان دادههای قابل مشاهده تضمین میکنند که رابط کاربری همیشه یک حالت رابط کاربری برای نمایش روی صفحه دارد |
از بین این دو، انتخاب API ناهمزمان برای ورودی تأثیر بیشتری بر ماهیت خط لوله تولید حالت دارد تا انتخاب API قابل مشاهده برای خروجی. این به این دلیل است که ورودی ها نوع پردازشی را که ممکن است در خط لوله اعمال شود دیکته می کنند .
مونتاژ خط لوله تولید دولتی
بخشهای بعدی تکنیکهای تولید حالت را پوشش میدهند که برای ورودیهای مختلف و APIهای خروجی مناسبتر هستند. هر خط لوله تولید دولتی ترکیبی از ورودی ها و خروجی ها است و باید:
- آگاه از چرخه حیات : در مواردی که رابط کاربری قابل مشاهده یا فعال نباشد، خط لوله تولید دولتی نباید هیچ منبعی را مصرف کند مگر اینکه صریحاً مورد نیاز باشد.
- مصرف آسان : رابط کاربری باید بتواند به راحتی حالت رابط کاربری تولید شده را ارائه دهد. ملاحظات مربوط به خروجی خط لوله تولید دولتی در بین API های View مختلف مانند سیستم View یا Jetpack Compose متفاوت است.
ورودی ها در خطوط لوله تولید دولتی
ورودیهای یک خط لوله تولید حالت ممکن است منابع تغییر حالت خود را از طریق:
- عملیات تک شات که ممکن است همزمان یا ناهمزمان باشند، برای مثال فراخوانی برای
suspend
توابع. - APIهای جریانی، برای مثال
Flows
. - همه موارد فوق.
بخشهای زیر نحوه جمعآوری خط لوله تولید دولتی را برای هر یک از ورودیهای بالا توضیح میدهند.
API های تک شات به عنوان منابع تغییر حالت
از MutableStateFlow
API به عنوان یک ظرف حالت قابل مشاهده و تغییرپذیر استفاده کنید. در برنامههای Jetpack Compose، میتوانید mutableStateOf
نیز در نظر بگیرید، مخصوصاً هنگام کار با Compose text API . هر دو API روشهایی را ارائه میدهند که بهروزرسانیهای اتمی ایمن را برای مقادیری که میزبانی میکنند امکان پذیر میسازند، چه بهروزرسانیها همزمان یا ناهمزمان باشند.
به عنوان مثال، بهروزرسانیهای حالت را در یک برنامه ساده تاساندازی در نظر بگیرید. هر بار انداختن تاس از طرف کاربر، متد همزمان Random.nextInt()
را فراخوانی می کند و نتیجه در حالت UI نوشته می شود.
StateFlow
data class DiceUiState(
val firstDieValue: Int? = null,
val secondDieValue: Int? = null,
val numberOfRolls: Int = 0,
)
class DiceRollViewModel : ViewModel() {
private val _uiState = MutableStateFlow(DiceUiState())
val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()
// Called from the UI
fun rollDice() {
_uiState.update { currentState ->
currentState.copy(
firstDieValue = Random.nextInt(from = 1, until = 7),
secondDieValue = Random.nextInt(from = 1, until = 7),
numberOfRolls = currentState.numberOfRolls + 1,
)
}
}
}
حالت نوشتن
@Stable
interface DiceUiState {
val firstDieValue: Int?
val secondDieValue: Int?
val numberOfRolls: Int?
}
private class MutableDiceUiState: DiceUiState {
override var firstDieValue: Int? by mutableStateOf(null)
override var secondDieValue: Int? by mutableStateOf(null)
override var numberOfRolls: Int by mutableStateOf(0)
}
class DiceRollViewModel : ViewModel() {
private val _uiState = MutableDiceUiState()
val uiState: DiceUiState = _uiState
// Called from the UI
fun rollDice() {
_uiState.firstDieValue = Random.nextInt(from = 1, until = 7)
_uiState.secondDieValue = Random.nextInt(from = 1, until = 7)
_uiState.numberOfRolls = _uiState.numberOfRolls + 1
}
}
تغییر وضعیت رابط کاربری از تماس های ناهمزمان
برای تغییرات حالتی که به نتیجه ناهمزمان نیاز دارند، یک Coroutine را در CoroutineScope
مناسب راه اندازی کنید. این به برنامه اجازه میدهد تا زمانی که CoroutineScope
لغو میشود، کار را کنار بگذارد. سپس دارنده حالت، نتیجه فراخوانی متد تعلیق را در API قابل مشاهده ای که برای نمایش وضعیت رابط کاربری استفاده می شود، می نویسد.
برای مثال، AddEditTaskViewModel
را در نمونه معماری در نظر بگیرید. هنگامی که متد saveTask()
معلق یک کار را به صورت ناهمزمان ذخیره می کند، روش update
در MutableStateFlow تغییر حالت را به حالت UI منتشر می کند.
StateFlow
data class AddEditTaskUiState(
val title: String = "",
val description: String = "",
val isTaskCompleted: Boolean = false,
val isLoading: Boolean = false,
val userMessage: String? = null,
val isTaskSaved: Boolean = false
)
class AddEditTaskViewModel(...) : ViewModel() {
private val _uiState = MutableStateFlow(AddEditTaskUiState())
val uiState: StateFlow<AddEditTaskUiState> = _uiState.asStateFlow()
private fun createNewTask() {
viewModelScope.launch {
val newTask = Task(uiState.value.title, uiState.value.description)
try {
tasksRepository.saveTask(newTask)
// Write data into the UI state.
_uiState.update {
it.copy(isTaskSaved = true)
}
}
catch(cancellationException: CancellationException) {
throw cancellationException
}
catch(exception: Exception) {
_uiState.update {
it.copy(userMessage = getErrorMessage(exception))
}
}
}
}
}
حالت نوشتن
@Stable
interface AddEditTaskUiState {
val title: String
val description: String
val isTaskCompleted: Boolean
val isLoading: Boolean
val userMessage: String?
val isTaskSaved: Boolean
}
private class MutableAddEditTaskUiState : AddEditTaskUiState() {
override var title: String by mutableStateOf("")
override var description: String by mutableStateOf("")
override var isTaskCompleted: Boolean by mutableStateOf(false)
override var isLoading: Boolean by mutableStateOf(false)
override var userMessage: String? by mutableStateOf<String?>(null)
override var isTaskSaved: Boolean by mutableStateOf(false)
}
class AddEditTaskViewModel(...) : ViewModel() {
private val _uiState = MutableAddEditTaskUiState()
val uiState: AddEditTaskUiState = _uiState
private fun createNewTask() {
viewModelScope.launch {
val newTask = Task(uiState.value.title, uiState.value.description)
try {
tasksRepository.saveTask(newTask)
// Write data into the UI state.
_uiState.isTaskSaved = true
}
catch(cancellationException: CancellationException) {
throw cancellationException
}
catch(exception: Exception) {
_uiState.userMessage = getErrorMessage(exception))
}
}
}
}
تغییر وضعیت رابط کاربری از رشته های پس زمینه
ترجیحاً Coroutines در توزیع کننده اصلی برای تولید حالت UI راه اندازی شود. یعنی خارج از بلوک withContext
در قطعه کد زیر. با این حال، اگر نیاز به به روز رسانی حالت UI در زمینه پس زمینه دیگری دارید، می توانید این کار را با استفاده از API های زیر انجام دهید:
- از متد
withContext
برای اجرای Coroutines در یک زمینه همزمان متفاوت استفاده کنید. - هنگام استفاده از
MutableStateFlow
، طبق معمول از روشupdate
استفاده کنید. - هنگام استفاده از Compose State، از
Snapshot.withMutableSnapshot
برای تضمین به روز رسانی اتمی State در زمینه همزمان استفاده کنید.
برای مثال، در قطعه DiceRollViewModel
زیر، فرض کنید که SlowRandom.nextInt()
یک تابع suspend
محاسباتی فشرده است که باید از یک Coroutine محدود به CPU فراخوانی شود.
StateFlow
class DiceRollViewModel(
private val defaultDispatcher: CoroutineScope = Dispatchers.Default
) : ViewModel() {
private val _uiState = MutableStateFlow(DiceUiState())
val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()
// Called from the UI
fun rollDice() {
viewModelScope.launch() {
// Other Coroutines that may be called from the current context
…
withContext(defaultDispatcher) {
_uiState.update { currentState ->
currentState.copy(
firstDieValue = SlowRandom.nextInt(from = 1, until = 7),
secondDieValue = SlowRandom.nextInt(from = 1, until = 7),
numberOfRolls = currentState.numberOfRolls + 1,
)
}
}
}
}
}
حالت نوشتن
class DiceRollViewModel(
private val defaultDispatcher: CoroutineScope = Dispatchers.Default
) : ViewModel() {
private val _uiState = MutableDiceUiState()
val uiState: DiceUiState = _uiState
// Called from the UI
fun rollDice() {
viewModelScope.launch() {
// Other Coroutines that may be called from the current context
…
withContext(defaultDispatcher) {
Snapshot.withMutableSnapshot {
_uiState.firstDieValue = SlowRandom.nextInt(from = 1, until = 7)
_uiState.secondDieValue = SlowRandom.nextInt(from = 1, until = 7)
_uiState.numberOfRolls = _uiState.numberOfRolls + 1
}
}
}
}
}
استریم APIها به عنوان منابع تغییر حالت
برای منابع تغییر حالت که مقادیر متعددی را در طول زمان در جریانها تولید میکنند، تجمیع خروجیهای همه منابع در یک کل منسجم یک رویکرد ساده برای تولید حالت است.
هنگام استفاده از Kotlin Flow، می توانید با تابع ترکیب به این هدف برسید. نمونه ای از این را می توان در نمونه "Now in Android" در InterestsViewModel مشاهده کرد:
class InterestsViewModel(
authorsRepository: AuthorsRepository,
topicsRepository: TopicsRepository
) : ViewModel() {
val uiState = combine(
authorsRepository.getAuthorsStream(),
topicsRepository.getTopicsStream(),
) { availableAuthors, availableTopics ->
InterestsUiState.Interests(
authors = availableAuthors,
topics = availableTopics
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = InterestsUiState.Loading
)
}
استفاده از عملگر stateIn
برای ایجاد StateFlows
به رابط کاربری کنترل دانهبندی دقیقتری بر فعالیت خط لوله تولید دولت میدهد، زیرا ممکن است تنها زمانی لازم باشد که رابط کاربری قابل مشاهده باشد فعال باشد.
- در صورتی که خط لوله فقط زمانی فعال باشد که UI در حین جمعآوری جریان به شیوهای آگاه از چرخه زندگی، از
SharingStarted.WhileSubscribed()
- از
SharingStarted.Lazily
استفاده کنید اگر خط لوله باید تا زمانی که کاربر ممکن است به رابط کاربری بازگردد فعال باشد، یعنی رابط کاربری در پشت پشتی یا در برگه دیگری خارج از صفحه است.
در مواردی که تجمیع منابع حالت مبتنی بر جریان اعمال نمیشود، APIهای جریان مانند Kotlin Flow مجموعهای غنی از تبدیلها مانند ادغام ، مسطح کردن و غیره را برای کمک به پردازش جریانها به حالت رابط کاربری ارائه میدهند.
APIهای تک شات و جریان به عنوان منابع تغییر حالت
در موردی که خط لوله تولید دولتی هم به تماس های تک شات و هم به جریان ها به عنوان منابع تغییر حالت بستگی دارد، جریان ها محدودیت تعیین کننده هستند. بنابراین، تماسهای تک شات را به APIهای جریانی تبدیل کنید، یا خروجی آنها را به جریانها منتقل کنید و پردازش را همانطور که در بخش جریانهای بالا توضیح داده شد، از سر بگیرید.
با جریان ها، این معمولاً به معنای ایجاد یک یا چند نمونه MutableStateFlow
پشتیبان خصوصی برای انتشار تغییرات حالت است. همچنین میتوانید جریانهای عکس فوری را از حالت Compose ایجاد کنید .
TaskDetailViewModel
از مخزن نمونه های معماری زیر در نظر بگیرید:
StateFlow
class TaskDetailViewModel @Inject constructor(
private val tasksRepository: TasksRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val _isTaskDeleted = MutableStateFlow(false)
private val _task = tasksRepository.getTaskStream(taskId)
val uiState: StateFlow<TaskDetailUiState> = combine(
_isTaskDeleted,
_task
) { isTaskDeleted, task ->
TaskDetailUiState(
task = taskAsync.data,
isTaskDeleted = isTaskDeleted
)
}
// Convert the result to the appropriate observable API for the UI
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = TaskDetailUiState()
)
fun deleteTask() = viewModelScope.launch {
tasksRepository.deleteTask(taskId)
_isTaskDeleted.update { true }
}
}
حالت نوشتن
class TaskDetailViewModel @Inject constructor(
private val tasksRepository: TasksRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private var _isTaskDeleted by mutableStateOf(false)
private val _task = tasksRepository.getTaskStream(taskId)
val uiState: StateFlow<TaskDetailUiState> = combine(
snapshotFlow { _isTaskDeleted },
_task
) { isTaskDeleted, task ->
TaskDetailUiState(
task = taskAsync.data,
isTaskDeleted = isTaskDeleted
)
}
// Convert the result to the appropriate observable API for the UI
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = TaskDetailUiState()
)
fun deleteTask() = viewModelScope.launch {
tasksRepository.deleteTask(taskId)
_isTaskDeleted = true
}
}
انواع خروجی در خطوط لوله تولید دولتی
انتخاب API خروجی برای وضعیت UI و ماهیت ارائه آن تا حد زیادی به APIی بستگی دارد که برنامه شما برای ارائه رابط کاربری استفاده می کند. در برنامههای اندروید، میتوانید از Views یا Jetpack Compose استفاده کنید. ملاحظات در اینجا عبارتند از:
- وضعیت خواندن به شیوه ای آگاه از چرخه زندگی .
- اینکه آیا حالت باید در یک یا چند فیلد از طرف دارنده حالت نمایش داده شود.
جدول زیر به طور خلاصه نشان می دهد که از چه APIهایی برای خط لوله تولید دولتی خود برای هر ورودی و مصرف کننده استفاده کنید:
ورودی | مصرف کننده | خروجی |
---|---|---|
API های تک شات | بازدیدها | StateFlow یا LiveData |
API های تک شات | نوشتن | StateFlow یا Compose State |
APIهای جریان | بازدیدها | StateFlow یا LiveData |
APIهای جریان | نوشتن | StateFlow |
APIهای تک شات و جریان | بازدیدها | StateFlow یا LiveData |
APIهای تک شات و جریان | نوشتن | StateFlow |
راه اندازی خط لوله تولید دولتی
راه اندازی خطوط لوله تولید دولتی شامل تنظیم شرایط اولیه برای اجرای خط لوله است. این ممکن است شامل ارائه مقادیر ورودی اولیه حیاتی برای شروع خط لوله باشد، به عنوان مثال یک id
برای نمای جزئیات یک مقاله خبری، یا شروع یک بار ناهمزمان.
برای حفظ منابع سیستم باید در صورت امکان خط لوله تولید دولتی را با تنبلی راه اندازی کنید. در عمل، این اغلب به معنای صبر کردن است تا زمانی که یک مصرف کننده از خروجی وجود داشته باشد. APIهای Flow
این امکان را با آرگومان started
در متد stateIn
می دهند. در مواردی که این مورد غیر قابل اجرا است، یک تابع initialize()
idempotent برای شروع صریح خط لوله تولید حالت تعریف کنید، همانطور که در قطعه زیر نشان داده شده است:
class MyViewModel : ViewModel() {
private var initializeCalled = false
// This function is idempotent provided it is only called from the UI thread.
@MainThread
fun initialize() {
if(initializeCalled) return
initializeCalled = true
viewModelScope.launch {
// seed the state production pipeline
}
}
}
نمونه ها
نمونه های گوگل زیر تولید حالت در لایه UI را نشان می دهد. برای دیدن این راهنمایی در عمل، آنها را کاوش کنید:
برای شما توصیه می شود
- توجه: وقتی جاوا اسکریپت خاموش است، متن پیوند نمایش داده می شود
- لایه رابط کاربری
- یک برنامه آفلاین بسازید
- دارندگان ایالت و حالت رابط کاربری {:#mad-arch}