Couche d'interface utilisateur (vues)

Concepts et implémentation de Jetpack Compose

Le rôle de l'UI est d'afficher les données de l'application à l'écran et de servir de point principal d'interaction utilisateur. Chaque fois que les données changent, que ce soit du fait d'une interaction de l'utilisateur (comme appuyer sur un bouton) ou d'une entrée externe (telle qu'une réponse du réseau), l'UI doit être mise à jour pour refléter ces modifications. En réalité, l'UI est une représentation visuelle de l'état de l'application, tel qu'il est extrait de la couche de données.

Cependant, le format des données d'application obtenues à partir de la couche de données est différent de celui des informations à afficher. Par exemple, vous n'aurez peut-être besoin que d'une partie des données de l'interface utilisateur ou vous devrez fusionner deux sources de données différentes pour présenter des informations pertinentes à l'utilisateur. Quelle que soit la logique que vous appliquez, vous devez transmettre à l'interface utilisateur toutes les informations nécessaires à son affichage complet. La couche d'UI est le pipeline qui convertit les modifications de données d'application dans un formulaire que l'UI peut présenter, puis les affiche.

Exposer l'état de l'interface utilisateur

Après avoir défini l'état de l'interface utilisateur et déterminé la manière dont vous allez gérer la production de cet état, l'étape suivante consiste à présenter l'état produit à l'interface utilisateur. Étant donné que vous utilisez la fonction définie par l'utilisateur pour gérer la production de l'état, vous pouvez considérer l'état produit comme un flux. En d'autres termes, plusieurs versions de l'état seront produites au fil du temps. Par conséquent, vous devez exposer l'état de l'interface utilisateur dans un conteneur de données observable, tel que LiveData ou StateFlow. En effet, l'UI peut ainsi réagir à toute modification d'état sans avoir à extraire manuellement les données directement à partir de ViewModel. Ces types ont également l'avantage de toujours mettre en cache la dernière version de l'état de l'interface utilisateur, ce qui est utile pour restaurer rapidement l'état après une modification de la configuration.

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

    val uiState: StateFlow<NewsUiState> = 
}

Une façon courante de créer un flux UiState consiste à exposer un flux modifiable secondaire en tant que flux immuable à partir de ViewModel, par exemple en exposant MutableStateFlow<UiState> en tant que StateFlow<UiState>.

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

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

    ...

}

ViewModel peut alors exposer des méthodes qui modifient en interne l'état en publiant les mises à jour que l'UI doit utiliser. Prenons, par exemple, le cas où une action asynchrone doit être effectuée. Une coroutine peut être lancée à l'aide de viewModelScope, et l'état modifiable peut être mis à jour une fois l'opération terminée.

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

Utiliser l'état de l'interface utilisateur

Lorsque vous utilisez des conteneurs de données observables dans l'UI, assurez-vous de prendre en compte le cycle de vie de l'interface utilisateur. C'est important, car l'UI ne doit pas observer l'état de l'UI lorsque la vue n'est pas présentée à l'utilisateur. Pour en savoir plus à ce sujet, consultez cet article de blog. Lorsque vous utilisez LiveData, LifecycleOwner se charge implicitement des problèmes de cycle de vie. Lorsque vous utilisez des flux, il est préférable de gérer cela avec le champ d'application de coroutine approprié et l'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
                }
            }
        }
    }
}

Afficher les opérations en cours

Un moyen simple de représenter les états de chargement dans une classe UiState consiste à utiliser un champ booléen :

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

La valeur de cette option représente la présence ou l'absence d'une barre de progression dans l'interface utilisateur.

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

Animations

Pour assurer des transitions de navigation fluides de premier niveau, vous pouvez attendre que le second écran charge des données avant de lancer l'animation. Le framework de vue Android fournit des hooks pour retarder les transitions entre les destinations de fragment avec les API postponeEnterTransition() et startPostponedEnterTransition(). Ces API permettent de s'assurer que les éléments de l'interface utilisateur du deuxième écran (généralement une image extraite du réseau) sont prêts à être affichés avant l'animation de la transition vers cet écran.