Camada de interface (Views)

Conceitos e implementação do Jetpack Compose

O papel da IU é mostrar os dados do app na tela e atuar como o ponto principal de interação do usuário. Sempre que os dados mudam, seja devido à interação do usuário (como o pressionamento de um botão) ou a entradas externas (como uma resposta de rede), a interface precisa ser atualizada para refletir essas mudanças. A IU é uma representação visual do estado do app recuperado da camada de dados.

No entanto, os dados do app recebidos da camada de dados costumam estar em um formato diferente das informações que são que precisam ser mostradas. Por exemplo, talvez você só precise de parte dos dados da IU ou tenha que combinar duas fontes de dados diferentes para apresentar informações relevantes ao usuário. Independentemente da lógica aplicada, você deve transmitir à IU todas as informações que ela precisa renderizar totalmente. A camada de IU é o pipeline que converte as mudanças de dados do app em um formato que a IU pode usar para que elas sejam mostradas.

Expor o estado da IU

Após definir o estado da IU e determinar como você vai gerenciar a produção desse estado, a próxima etapa é apresentar o estado produzido à IU. Como você está usando o UDF para gerenciar a produção do estado, pode considerar o estado produzido como um fluxo. Em outras palavras, várias versões dele serão produzidas ao longo do tempo. Assim, você precisa expor o estado da IU em um detentor de dados observáveis, como LiveData ou StateFlow. O motivo é que a IU pode reagir a qualquer mudança feita no estado sem precisar extrair dados de forma manual diretamente da ViewModel. Esses tipos também têm o benefício de sempre ter a versão mais recente do estado da IU armazenada em cache, o que é útil para uma restauração rápida dele após mudanças de configuração.

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

    val uiState: StateFlow<NewsUiState> = 
}

Uma maneira comum de criar um fluxo da classe UiState é expor um fluxo mutável de apoio como um fluxo imutável da ViewModel. Por exemplo, expor MutableStateFlow<UiState> como StateFlow<UiState>.

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

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

    ...

}

A ViewModel pode expor métodos que modificam internamente o estado, publicando atualizações para que a IU consuma. Considere, por exemplo, o caso em que uma ação assíncrona precisa ser realizada. Uma corrotina pode ser iniciada usando a propriedade viewModelScope, e o estado mutável pode ser atualizado após a conclusão.

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

Consumir o estado da IU

Ao consumir detentores de dados observáveis na IU, considere o ciclo de vida dela. Isso é importante porque a interface não pode observar o estado dela quando a visualização não está sendo mostrada para o usuário. Para saber mais sobre esse assunto, consulte esta postagem do blog (em inglês). Ao usar o LiveData, a interface LifecycleOwner cuida das questões do ciclo de vida de forma implícita. Ao usar fluxos, é recomendável gerenciar isso com o escopo adequado de corrotinas e a 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
                }
            }
        }
    }
}

Mostrar operações em andamento

Uma maneira simples de representar os estados de carregamento em uma classe UiState é com um campo booleano:

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

O valor dessa sinalização representa a presença ou ausência de uma barra de progresso na interface.

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

Animações

Para fornecer transições de navegação de nível superior fluidas e suaves, recomendamos que você aguarde a segunda tela carregar os dados antes de iniciar a animação. O framework de visualização do Android fornece hooks para atrasar transições entre destinos de fragmento com as APIs postponeEnterTransition() e startPostponedEnterTransition(). Essas APIs fornecem uma maneira de garantir que os elementos da IU na segunda tela, normalmente uma imagem buscada na rede, estejam prontos para serem mostrados antes da animação da transição para essa tela.