تولید دولتی UI

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

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

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

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

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

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

رویدادها در مقابل ایالت
شکل 1 : رویدادها باعث تغییر حالت می شوند

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

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

خط لوله تولید دولتی UI

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

  • ورودی ها : منابع تغییر حالت. ممکنه باشند:
    • Local to layer UI: اینها می توانند رویدادهای کاربر مانند وارد کردن عنوانی برای یک "to-do" در یک برنامه مدیریت کار یا API هایی باشند که دسترسی به منطق UI را فراهم می کنند که باعث ایجاد تغییرات در وضعیت UI می شود. به عنوان مثال، فراخوانی متد open در DrawerState در Jetpack Compose.
    • خارجی به لایه UI: اینها منابعی از دامنه یا لایه های داده هستند که باعث تغییر در وضعیت UI می شوند. به عنوان مثال اخباری که بارگیری از یک NewsRepository یا رویدادهای دیگر به پایان رسید.
    • مخلوطی از تمام موارد بالا.
  • دارندگان حالت : انواعی که منطق تجاری و/یا منطق رابط کاربری را در منابع تغییر حالت اعمال می‌کنند و رویدادهای کاربر را برای تولید وضعیت رابط کاربری پردازش می‌کنند.
  • خروجی : حالت رابط کاربری که برنامه می تواند برای ارائه اطلاعات مورد نیاز کاربران ارائه دهد.
خط لوله تولید دولتی
شکل 2 : خط لوله تولید دولتی

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 برای اجرای کوروتین ها در یک زمینه همزمان متفاوت استفاده کنید.
  • هنگام استفاده از 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 را نشان می دهد. برای دیدن این راهنمایی در عمل، آنها را کاوش کنید:

{% کلمه به کلمه %} {% آخر کلمه %} {% کلمه به کلمه %} {% آخر کلمه %}