تولید دولتی UI

رابط‌های کاربری مدرن به ندرت ایستا هستند. وضعیت رابط کاربری زمانی تغییر می‌کند که کاربر با رابط کاربری تعامل داشته باشد یا زمانی که برنامه نیاز به نمایش داده‌های جدید داشته باشد.

این سند دستورالعمل‌هایی را برای تولید و مدیریت وضعیت رابط کاربری (UI state) تعیین می‌کند. هدف آن کمک به شما در درک موارد زیر است:

  • از چه APIهایی برای تولید وضعیت رابط کاربری استفاده کنیم. این بستگی به ماهیت منابع تغییر وضعیت موجود در نگهدارنده‌های وضعیت شما دارد، که از اصول جریان داده یک‌طرفه پیروی می‌کند.
  • چگونه می‌توان تولید وضعیت رابط کاربری را محدود کرد تا از منابع سیستم آگاه باشیم.
  • چگونه وضعیت رابط کاربری را برای مصرف توسط رابط کاربری نمایش دهیم.

اساساً، تولید وضعیت، اعمال تدریجی این تغییرات در وضعیت رابط کاربری است. وضعیت همیشه وجود دارد و در نتیجه رویدادها تغییر می‌کند. تفاوت‌های بین رویدادها و وضعیت در جدول زیر خلاصه شده است:

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

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

رویدادها در مقابل ایالت
شکل ۱ : رویدادها باعث تغییر وضعیت می‌شوند

رویدادها می‌توانند از موارد زیر ناشی شوند:

  • کاربران : همانطور که با رابط کاربری برنامه تعامل دارند.
  • منابع دیگر تغییر وضعیت : APIهایی که داده‌های برنامه را از رابط کاربری، دامنه یا لایه‌های داده مانند رویدادهای زمان‌بندی Snackbar، موارد استفاده یا مخازن ارائه می‌دهند.

خط تولید وضعیت UI

تولید وضعیت در برنامه‌های اندروید را می‌توان به عنوان یک خط لوله پردازشی شامل موارد زیر در نظر گرفت:

  • ورودی‌ها : منابع تغییر وضعیت. آن‌ها می‌توانند موارد زیر باشند:
    • محلی برای لایه رابط کاربری: این موارد ممکن است رویدادهای کاربری مانند وارد کردن عنوان برای یک «کار» در یک برنامه مدیریت وظیفه توسط کاربر یا APIهایی باشند که دسترسی به منطق رابط کاربری را فراهم می‌کنند که باعث ایجاد تغییرات در وضعیت رابط کاربری می‌شود - برای مثال، فراخوانی متد open در DrawerState در Jetpack Compose.
    • منابع خارجی نسبت به لایه رابط کاربری: اینها منابعی از دامنه یا لایه‌های داده هستند که باعث تغییر در وضعیت رابط کاربری می‌شوند - برای مثال، اخباری که بارگیری آنها از NewsRepository یا رویدادهای دیگر به پایان رسیده است.
    • مخلوطی از موارد فوق.
  • دارندگان وضعیت : انواعی که منطق کسب‌وکار و منطق رابط کاربری را بر منابع تغییر وضعیت اعمال می‌کنند و رویدادهای کاربر را برای تولید وضعیت رابط کاربری پردازش می‌کنند.
  • خروجی : حالت رابط کاربری که برنامه می‌تواند برای ارائه اطلاعات مورد نیاز کاربران، رندر کند.
خط تولید دولتی
شکل ۲ : خط تولید ایالتی

API های تولید دولتی

بسته به اینکه در چه مرحله‌ای از خط تولید هستید، دو API اصلی در تولید وضعیت (state production) استفاده می‌شوند:

مرحله خط لوله رابط برنامه‌نویسی کاربردی
ورودی از APIهای ناهمزمان مانند Coroutineها و Flowها برای انجام کارها خارج از نخ UI استفاده کنید تا UI بدون مشکل باقی بماند.
خروجی از APIهای نگهدارنده داده قابل مشاهده مانند Compose State یا StateFlow برای نامعتبر کردن و رندر مجدد رابط کاربری هنگام تغییر وضعیت استفاده کنید. نگهدارنده‌های داده قابل مشاهده تضمین می‌کنند که رابط کاربری همیشه یک وضعیت رابط کاربری برای نمایش روی صفحه دارد.

انتخاب API ناهمزمان برای ورودی، تأثیر بیشتری بر ماهیت خط تولید حالت نسبت به انتخاب API قابل مشاهده برای خروجی دارد. دلیل این امر این است که ورودی‌ها نوع پردازشی را که می‌تواند روی خط تولید اعمال شود، تعیین می‌کنند .

مونتاژ خط لوله تولید دولتی

بخش‌های بعدی تکنیک‌های تولید وضعیت را که برای ورودی‌های مختلف مناسب‌تر هستند و APIهای خروجی که با آنها مطابقت دارند، پوشش می‌دهد. هر خط تولید وضعیت ترکیبی از ورودی‌ها و خروجی‌ها است و باید موارد زیر را داشته باشد:

  • آگاه از چرخه حیات : در مواردی که رابط کاربری قابل مشاهده یا فعال نیست، خط تولید وضعیت نباید هیچ منبعی را مصرف کند، مگر اینکه صریحاً لازم باشد.
  • مصرف آسان : رابط کاربری باید بتواند به راحتی حالت رابط کاربری تولید شده را رندر کند. در Jetpack Compose، مصرف حالت برای رابط کاربری بسیار مهم است، زیرا composableها می‌توانند بر اساس تغییرات حالت به‌روزرسانی شوند.

ورودی‌ها در خطوط تولید ایالتی

ورودی‌ها در یک خط تولید وضعیت، منابع تغییر وضعیت خود را از طریق موارد زیر فراهم می‌کنند:

  • عملیات‌های تک‌مرحله‌ای که می‌توانند همزمان یا ناهمزمان باشند - برای مثال، فراخوانی‌ها برای suspend توابع.
  • APIهای جریان - برای مثال، Flows .
  • همه موارد فوق.

بخش‌های بعدی نحوه‌ی مونتاژ یک خط تولید وضعیت برای هر یک از ورودی‌های فوق را پوشش می‌دهند.

APIهای تک‌مرحله‌ای به عنوان منابع تغییر وضعیت

مدیریت وضعیت با نگهدارنده‌های داده قابل مشاهده. از API mutableStateOf استفاده کنید، به خصوص هنگام کار با APIهای متنی Compose . برای مدیریت وضعیت پیچیده‌تر یا هنگام ادغام با سایر اجزای معماری، از API MutableStateFlow استفاده کنید. هر دو API روش‌هایی را ارائه می‌دهند که امکان به‌روزرسانی‌های اتمی ایمن برای مقادیری که میزبانی می‌کنند را فراهم می‌کنند، چه به‌روزرسانی‌ها همزمان باشند و چه ناهمزمان.

برای مثال، به‌روزرسانی‌های وضعیت را در یک برنامه‌ی ساده‌ی پرتاب تاس در نظر بگیرید. هر پرتاب تاس از طرف کاربر، متد Random.nextInt همزمان را فراخوانی می‌کند و نتیجه در وضعیت رابط کاربری نوشته می‌شود.

حالت نوشتن

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

جریان حالت

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

تغییر وضعیت رابط کاربری از فراخوانی‌های ناهمزمان

برای تغییرات وضعیتی که نیاز به نتیجه‌ای ناهمزمان دارند، یک Coroutine را در CoroutineScope مناسب اجرا کنید. این کار به برنامه اجازه می‌دهد تا هنگام لغو CoroutineScope کار را کنار بگذارد. سپس دارنده وضعیت، نتیجه فراخوانی متد suspend را در API قابل مشاهده‌ای که برای نمایش وضعیت UI استفاده می‌شود، می‌نویسد.

برای مثال، AddEditTaskViewModel در نمونه معماری در نظر بگیرید. وقتی متد saveTask در حال تعلیق، یک وظیفه را به صورت ناهمگام ذخیره می‌کند، متد update در MutableStateFlow تغییر حالت را به حالت رابط کاربری منتقل می‌کند.

حالت نوشتن

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

جریان حالت

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

تغییر وضعیت رابط کاربری از نخ‌های پس‌زمینه

ترجیح داده می‌شود که Coroutineها را برای تولید وضعیت UI روی توزیع‌کننده اصلی اجرا کنید - یعنی خارج از بلوک withContext در قطعه کد زیر. با این حال، اگر نیاز دارید وضعیت UI را در یک زمینه پس‌زمینه متفاوت به‌روزرسانی کنید، می‌توانید موارد زیر را انجام دهید:

  • از متد withContext برای اجرای Coroutineها در یک context همزمان متفاوت استفاده کنید.
  • هنگام استفاده از MutableStateFlow ، طبق معمول از متد update استفاده کنید.
  • هنگام استفاده از Compose State، از متد Snapshot.withMutableSnapshot برای تضمین به‌روزرسانی‌های اتمی State در زمینه همزمان استفاده کنید.

برای مثال، فرض کنید که در قطعه کد DiceRollViewModel زیر، تابع SlowRandom.nextInt یک تابع suspend با محاسبات فشرده است که باید از یک Coroutine متصل به CPU فراخوانی شود.

حالت نوشتن

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

جریان حالت

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

APIهای جریانی به عنوان منابع تغییر وضعیت

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

هنگام استفاده از Kotlin Flows، می‌توانید با استفاده از تابع combine به این هدف دست یابید. نمونه‌ای از این مورد را می‌توانید در نمونه "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 به رابط کاربری کنترل دقیق‌تری بر فعالیت خط تولید حالت می‌دهد، زیرا ممکن است فقط زمانی که رابط کاربری قابل مشاهده است، لازم باشد فعال باشد.

  • اگر لازم است که pipeline فقط زمانی فعال باشد که رابط کاربری (UI) در حین جمع‌آوری جریان به شیوه‌ای آگاه از چرخه حیات (lifecycle-aware) قابل مشاهده باشد، SharingStarted.WhileSubscribed استفاده کنید.
  • اگر لازم است که pipeline تا زمانی که کاربر ممکن است به رابط کاربری برگردد فعال باشد - یعنی رابط کاربری در backstack یا در تب دیگری خارج از صفحه نمایش باشد - SharingStarted.Lazily استفاده کنید.

در مواردی که تجمیع منابع حالت مبتنی بر جریان امکان‌پذیر نباشد، APIهای جریان مانند Kotlin Flows مجموعه‌ای غنی از تبدیل‌ها مانند ادغام ، مسطح‌سازی و غیره را برای کمک به پردازش جریان‌ها به حالت رابط کاربری ارائه می‌دهند.

APIهای تک‌مرحله‌ای و جریانی به عنوان منابع تغییر وضعیت

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

در مورد جریان‌ها، این معمولاً به معنای ایجاد یک یا چند نمونه خصوصی MutableStateFlow پشتیبان برای انتشار تغییرات حالت است. همچنین می‌توانید جریان‌های snapshot را از حالت Compose ایجاد کنید .

TaskDetailViewModel را از مخزن architecture-samples در نظر بگیرید. وضعیت رابط کاربری به یک جریان برای وظیفه فعلی ( _task ) و یک منبع تک‌مرحله‌ای ( _isTaskDeleted ) بستگی دارد که هنگام حذف وظیفه به‌روزرسانی می‌شود. این پرچم برای تمایز بین زمانی که یک وظیفه به دلیل شناسه نادرست در پایگاه داده یافت نمی‌شود و زمانی که به دلیل حذف کاربر یافت نمی‌شود، ضروری است:

حالت نوشتن

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, taskAsync ->
        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
    }
}

جریان حالت

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, taskAsync ->
        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 }
    }
}

انواع خروجی در خطوط تولید ایالتی

انتخاب API خروجی برای وضعیت رابط کاربری و ماهیت ارائه آن تا حد زیادی به API مورد استفاده برنامه شما برای رندر رابط کاربری، مانند Compose، بستگی دارد. Jetpack Compose ابزار مدرن پیشنهادی برای ساخت رابط کاربری بومی است. ملاحظات در اینجا شامل موارد زیر است:

جدول زیر خلاصه‌ای از APIهایی که باید برای خط تولید حالت خود هنگام استفاده از Jetpack Compose استفاده کنید را ارائه می‌دهد:

ورودی خروجی
API های تک مرحله ای StateFlow یا Compose State
APIهای جریان StateFlow
APIهای تک‌مرحله‌ای و جریانی StateFlow

مقداردهی اولیه خط تولید ایالتی

مقداردهی اولیه‌ی خطوط تولید وضعیت شامل تنظیم شرایط اولیه برای اجرای خط تولید است. این ممکن است شامل ارائه مقادیر ورودی اولیه‌ای باشد که برای شروع خط تولید حیاتی هستند - برای مثال، یک id برای نمای جزئیات یک مقاله خبری یا شروع یک بارگذاری ناهمزمان.

در صورت امکان، خط تولید حالت را به صورت تنبل مقداردهی اولیه کنید تا منابع سیستم حفظ شود. در عمل، این اغلب به معنای انتظار تا زمانی است که یک مصرف‌کننده برای خروجی وجود داشته باشد. APIهای Flow این امکان را با آرگومان started در متد stateIn فراهم می‌کنند. در مواردی که این امکان وجود ندارد، یک تابع idempotent initialize تعریف کنید تا خط تولید حالت را به طور صریح شروع کند، همانطور که در قطعه کد زیر نشان داده شده است:

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

نمونه‌ها

نمونه‌های گوگل زیر، تولید حالت در لایه رابط کاربری را نشان می‌دهند. برای مشاهده این راهنمایی در عمل، آنها را بررسی کنید:

منابع اضافی

برای اطلاعات بیشتر در مورد وضعیت رابط کاربری، به منابع اضافی زیر مراجعه کنید:

مستندات

محتوا را مشاهده می‌کند

{% کلمه به کلمه %} {% فعل کمکی %} {% کلمه به کلمه %} {% فعل کمکی %}