Слой пользовательского интерфейса (представления)

Концепции и реализация Jetpack Compose

Роль пользовательского интерфейса (UI) заключается в отображении данных приложения на экране, а также в том, чтобы служить основной точкой взаимодействия с пользователем. Всякий раз, когда данные изменяются, будь то в результате взаимодействия пользователя (например, нажатия кнопки) или внешнего ввода (например, ответа сети), UI должен обновляться, отражая эти изменения. По сути, UI представляет собой визуальное отображение состояния приложения, полученного из уровня данных.

Однако данные приложения, получаемые из слоя данных, обычно имеют другой формат, чем информация, которую необходимо отобразить. Например, для пользовательского интерфейса может потребоваться только часть данных, или же может потребоваться объединить два разных источника данных, чтобы представить информацию, актуальную для пользователя. Независимо от применяемой логики, необходимо передать пользовательскому интерфейсу всю необходимую информацию для полного отображения. Слой пользовательского интерфейса — это конвейер, который преобразует изменения данных приложения в форму, которую может представить пользовательский интерфейс, а затем отображает её.

Отобразить состояние пользовательского интерфейса

После определения состояния пользовательского интерфейса и способа управления его формированием, следующим шагом является представление сформированного состояния интерфейсу. Поскольку для управления формированием состояния используются пользовательские функции (UDF), сформированное состояние можно рассматривать как поток — другими словами, со временем будет создаваться несколько версий состояния. В результате следует предоставлять доступ к состоянию пользовательского интерфейса через наблюдаемый объект, такой как LiveData или StateFlow . Это необходимо для того, чтобы пользовательский интерфейс мог реагировать на любые изменения состояния без необходимости вручную извлекать данные непосредственно из ViewModel. Кроме того, эти типы данных имеют преимущество в том, что всегда кэшируют последнюю версию состояния пользовательского интерфейса, что полезно для быстрого восстановления состояния после изменений конфигурации.

class NewsViewModel(...) : ViewModel() {

    val uiState: StateFlow<NewsUiState> = 
}

Распространенный способ создания потока UiState — это предоставление доступа к базовому изменяемому потоку в виде неизменяемого потока из ViewModel, например, предоставление доступа MutableStateFlow<UiState> как StateFlow<UiState> .

class NewsViewModel(...) : ViewModel() {

    private val _uiState = MutableStateFlow(NewsUiState())
    val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

    ...

}

Затем ViewModel может предоставлять методы, которые внутренне изменяют состояние, публикуя обновления для пользовательского интерфейса. Например, в случае, когда необходимо выполнить асинхронное действие, можно запустить сопрограмму с помощью viewModelScope , и изменяемое состояние можно обновить по завершении.

class NewsViewModel(
    private val repository: NewsRepository,
    ...
) : ViewModel() {

    private val _uiState = MutableStateFlow(NewsUiState())
    val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

    private var fetchJob: Job? = null

    fun fetchArticles(category: String) {
        fetchJob?.cancel()
        fetchJob = viewModelScope.launch {
            try {
                val newsItems = repository.newsItemsForCategory(category)
                _uiState.update {
                    it.copy(newsItems = newsItems)
                }
            } catch (ioe: IOException) {
                // Handle the error and notify the UI when appropriate.
                _uiState.update {
                    val messages = getMessagesFromThrowable(ioe)
                    it.copy(userMessages = messages)
                }
            }
        }
    }
}

Потребление состояния пользовательского интерфейса

При использовании наблюдаемых объектов в пользовательском интерфейсе обязательно учитывайте жизненный цикл интерфейса. Это важно, поскольку интерфейс не должен отслеживать свое состояние, когда представление не отображается пользователю. Подробнее об этом можно узнать в этой статье блога . При использовании LiveData , LifecycleOwner неявно обрабатывает вопросы жизненного цикла. При использовании потоков лучше всего обрабатывать это с помощью соответствующей области видимости сопрограммы и API repeatOnLifecycle :

class NewsActivity : AppCompatActivity() {

    private val viewModel: NewsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // Update UI elements
                }
            }
        }
    }
}

Показать текущие операции

Простой способ представления состояний загрузки в классе UiState — использование логического поля:

data class NewsUiState(
    val isFetchingArticles: Boolean = false,
    ...
)

Значение этого флага указывает на наличие или отсутствие индикатора выполнения в пользовательском интерфейсе.

class NewsActivity : AppCompatActivity() {

    private val viewModel: NewsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Bind the visibility of the progressBar to the state
                // of isFetchingArticles.
                viewModel.uiState
                    .map { it.isFetchingArticles }
                    .distinctUntilChanged()
                    .collect { progressBar.isVisible = it }
            }
        }
    }
}

Анимации

Для обеспечения плавных и равномерных переходов между элементами навигации верхнего уровня может потребоваться дождаться загрузки данных на второй экран перед началом анимации. Фреймворк Android View предоставляет механизмы задержки переходов между фрагментами с помощью API-функций postponeEnterTransition() и startPostponedEnterTransition() . Эти API-функции позволяют гарантировать, что элементы пользовательского интерфейса на втором экране (обычно изображение, полученное из сети) будут готовы к отображению до того, как пользовательский интерфейс начнет анимацию перехода на этот экран.

{% verbatim %} {% endverbatim %} {% verbatim %} {% endverbatim %}