Implémenter les données de la liste de mise en cache dans Compose pour améliorer l'expérience utilisateur (Stale-while-revalidate)

avant-propos

Récemment, j'ai utilisé Compose pour implémenter un client Github APP pendant mon temps libre.

La référence est l'application Github mise en œuvre par le patron de GSY en utilisant une variété de cadres de langage différents.

Certains problèmes ont été rencontrés lors du processus de mise en œuvre, car presque toutes les données de ce client proviennent de l'API Github, de sorte que le rendu de l'interface utilisateur est également extrêmement dépendant des données demandées.

Et pour des raisons bien connues, lorsque nous utilisons l'API Github, la vitesse est anxiogène et nous ne pouvons même pas obtenir les données directement.

Cela rendra l'application que j'ai écrite très "bloquée" au niveau de l'utilisateur.

En fait, ce n'est pas que l'APP est bloquée, c'est juste que les données ne sont pas chargées.

Alors, comment devrions-nous résoudre ce problème ?

J'ai maintenant deux solutions :

  1. Ajoutez des animations de chargement, des effets de transition, etc.
  2. Augmentez le cache de données, affichez d'abord les données mises en cache lors de la demande de données, puis mettez à jour les dernières données dans l'interface utilisateur après avoir demandé les données

Pour la solution 1, Compose dispose déjà d'une solution mature utilisable, mais pour la solution 2, nous devons l'implémenter nous-mêmes.

Analyse des besoins en données de cache

Dans mon application Github, l'affichage des données est principalement réalisé en utilisant SwipeRefreshla coopération LazyColumnet paging3.

Remarque : SwipeRefreshest accompanistl'une des bibliothèques du projet, qui a été marquée comme obsolète

L'effet est à peu près le suivant :

1.png

On peut voir que cliquer sur l'APP et entrer dans la page d'accueil est une page pour afficher les données dynamiques de l'utilisateur actuel. Si nous ne faisons pas de mise en cache à ce moment, alors ce que nous voyons après être entré dans l'APP sera vide (ou chargement de l'animation), ce qui n'est évidemment pas très convivial pour l'utilisateur.

L'ajout de cache est donc très nécessaire.

Comme mentionné ci-dessus, lorsque j'ai écrit cette page, j'avais l'habitude d' paging3implémenter des données de chargement de page.

En fait, dans paging3la prise en charge des sources de données fournie par , il prend non seulement en charge l'obtention de données à partir du réseau, mais prend également en charge la liaison roompour réaliser la mise en cache des données locales ( RemoteMediator) :

pagination3-layered-architecture.svg

RemoteMediatorLorsque les données sont nécessaires, les données seront paging3interrogées à partir du cache de la base de données et renvoyées à paging3. Ce n'est que lorsque les données du cache sont épuisées ou que les données sont obsolètes et que l'utilisateur les actualise manuellement que de nouvelles données seront demandées au réseau.

L'utilisation de ce schéma garantit que paging3la base de données locale est toujours utilisée comme seule source de données :

Une RemoteMediatorimplémentation aide à charger les données paginées du réseau dans la base de données, mais ne charge pas les données directement dans l'interface utilisateur. Au lieu de cela, l'application utilise la base de données comme source de vérité. En d'autres termes, l'application n'affiche que les données mises en cache dans la base de données. Une PagingSourceimplémentation (par exemple, une implémentation générée par Room) gère le chargement des données mises en cache de la base de données dans l'interface utilisateur.

Parce que cette solution n'est pas l'objet de ce dont nous allons parler aujourd'hui, je ne la répéterai donc pas ici. Si vous en avez besoin, vous pouvez lire un autre article que j'ai écrit auparavant : Utiliser Compose pour implémenter des applications TODO basées sur l'architecture MVI , retrofit2 et widgets de prise en charge

Grâce à la description générale ci-dessus, nous pouvons clairement RemoteMediatorgarantir que la seule source de données est la base de données locale, et n'obtenir des données du réseau et les remplir dans le cache local que lorsque les données sont insuffisantes ou actualisées manuellement.

Cela posera un problème. Si nous devons garantir l'expérience utilisateur et toujours afficher les données lors de l'ouverture de l'APP, nous pouvons uniquement lui permettre de ne pas s'actualiser activement lors de l'initialisation. De cette façon, les données mises en cache localement seront toujours utilisées au lieu paging3de De nouvelles données, qui ne répondent évidemment pas à notre scénario d'exigences relativement élevées en matière d'actualité des données.

Ensuite, nous pouvons peut-être activer l'actualisation des données à chaque initialisation, mais cela équivaut à redemander des données réseau chaque fois que vous entrez dans l'application sans utiliser de données en cache, ce qui équivaut à ne pas utiliser de cache du tout, ce qui est toujours le même aux yeux de utilisateurs Il est simplement "vide" lorsqu'il est ouvert.

En résumé, RemoteMediatoril ne répond pas à nos besoins.

En faisant référence à l'application GitHub du patron de GSY, j'ai constaté qu'il n'utilisait aucun framework pour répondre aux exigences de cache que j'ai mentionnées, mais qu'il écrivait lui-même un ensemble de logique de chargement de cache.

Sa logique est également très simple à comprendre : lors du chargement des données, vérifiez d'abord si la base de données locale a un cache, et s'il y a un cache, sortez d'abord le cache et affichez-le. Ensuite, qu'il y ait ou non un cache, la demande de réseau est envoyée immédiatement après la fin du cache de requête, et lorsque les données de la demande de réseau sont reçues, elles sont d'abord mises en cache dans la base de données locale, puis l'interface utilisateur actuelle est remplacée par nouvelles données.

Je dois dire que cette idée est très claire et très adaptée à mes besoins.

Obsolète pendant la revalidation

Plus tard, j'ai vérifié beaucoup d'informations, dans l'intention de trouver un framework capable de réaliser cette logique, mais après avoir cherché, je n'ai pas trouvé Compose ou les frameworks associés disponibles pour Android.

Au lieu de cela, j'ai trouvé le nom de cette logique de mise en cache : Stale-while-revalidate .

Il s'avère que cette exigence a son propre nom, et son idée de base est également très simple :

La directive stale-while-revalidate indique à CloudFront de fournir immédiatement des réponses obsolètes aux utilisateurs pendant qu'il revalide les caches en arrière-plan. La directive stale-if-error définit la durée pendant laquelle CloudFront doit réutiliser les réponses obsolètes en cas d'erreur, ce qui offre une meilleure expérience utilisateur.

En termes simples, il s'agit d'utiliser d'abord les anciennes données (données de cache) tout en demandant de nouvelles données en arrière-plan.

Eh bien, puisqu'il n'y a pas de framework prêt à l'emploi, nous ne pouvons que l'implémenter nous-mêmes.

Implémenter le cache de données de requête de Compose

Remarque : Cette section suppose que le lecteur comprend déjà l'utilisation de base de la pagination3

Comme nous devons d'abord afficher les données mises en cache lors de la demande, nous définissons d'abord une donnée générale comme celle-ciLazyColumn :

@Composable
private fun <T: BaseUIModel>BasePagingLazyColumn(
    pagingItems: LazyPagingItems<T>?,
    cacheItems: List<T>? = null,
    itemUi: @Composable ColumnScope.(data: T) -> Unit,
) {
    
    
    LazyColumn(
        modifier = Modifier
            .fillMaxSize()
            .padding(bottom = 2.dp)
    ) {
    
    

        if (pagingItems == null) {
    
    
            item {
    
    
                Text(text = "No Data")
            }
        }
        else {
    
    
            val count = cacheItems?.size ?: pagingItems.itemCount
            items(count, key = {
    
    
                if (cacheItems == null) pagingItems.peek(it)!!.lazyColumnKey else cacheItems[it].lazyColumnKey
            }) {
    
    
                val item = if (cacheItems == null) pagingItems[it] else cacheItems[it]
                if (item != null) {
    
    
                    Column{
    
    
                        itemUi(data = item)
                    }
                }
            }

            if (pagingItems.itemCount < 1) {
    
    
                if (pagingItems.loadState.refresh == LoadState.Loading) {
    
    
                    item {
    
    
                        Text(text = "Loading...")
                    }
                }
                else {
    
    
                    item {
    
    
                        Text(text = "No More data")
                    }
                }
            }
        }
    }
}

Dans cette fonction, nous recevons trois paramètres :

  • pagingItemsAutrement dit, les dernières données chargées à partir du serveur renvoyées par la pagination
  • cacheItemsc'est-à-dire des données mises en cache localement
  • itemUiAutrement dit, l'interface utilisateur à afficher

Ensuite, tant que cacheItemsn'est pas vide, nous afficherons cacheItemsles données en premier, et cacheItemsn'afficherons les données que si est vide pagingItems.

Après cela, nous devons implémenter paging, PagingSourceici je choisis la méthode relativement simple d'obtention des commentaires ISSUE dans Github APP comme exemple :

class IssueCommentsPagingSource(
    private val issueService: IssueService,
    private val dataBase: CacheDB,
    private val onLoadFirstPageSuccess: () -> Unit
): PagingSource<Int, IssueUIModel>() {
    
    

    // ……
    
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, IssueUIModel> {
    
    
        try {
    
    
            val nextPageNumber = params.key ?: 1
            val response = issueService.getIssueComments()

            // ……

            val issueUiModel = response.body()

            if (nextPageNumber == 1) {
    
     // 缓存第一页
                dataBase.cacheDB().insertIssueComment(
                    DBIssueComment(
                        // ……
                    )
                )

                if (!issueUiModel.isNullOrEmpty()) {
    
    
                    onLoadFirstPageSuccess()
                }
            }

            return LoadResult.Page(
                data = issueUiModel ?: listOf(),
                prevKey = null, // 设置为 null 表示只加载下一页
                nextKey = if (nextPageNumber >= totalPage || totalPage == -1) null else nextPageNumber + 1
            )
        } catch (e: Exception) {
    
    
            return LoadResult.Error(e)
        }
    }

    // ……
}

Pour éviter toute confusion, j'ai omis la majeure partie du code non critique.

Dans le code ici, nous utilisons d'abord issueService.getIssueComments()pour obtenir la dernière liste de commentaires, puis jugeons s'il s'agit de la première page de données chargée, la mettons en cache dans la base de données dataBase.cacheDB().insertIssueComment(DBIssueComment(issueUiModel))et rappelons onLoadFirstPageSuccess()la fonction, qui est utilisée dans la logique métier pour gérer des opérations telles que que la mise à jour de l'interface utilisateur.

Ensuite, dans notre VIewModel, nous écrivons le code d'acquisition de données comme ceci :

var isInit = false

private suspend fun loadCommentData() {
    
    
    
    val cacheData = dataBase.cacheDB().queryIssueComment(
        // ……
    )
    if (!cacheData.isNullOrEmpty()) {
    
    
        val body = cacheData[0].data?.fromJson<List<IssueEvent>>()
        if (body != null) {
    
    
            Log.i("el", "refreshData: 使用缓存数据")
            viewStates = viewStates.copy(cacheCommentList = body )
        }
    }

    issueCommentFlow = Pager(
        PagingConfig(pageSize = AppConfig.PAGE_SIZE, initialLoadSize = AppConfig.PAGE_SIZE)
    ) {
    
    
        IssueCommentsPagingSource(
            // ……
        ) {
    
    
            viewStates = viewStates.copy(cacheCommentList = null)
            isInit = true
        }
    }.flow.cachedIn(viewModelScope)

    viewStates = viewStates.copy(issueCommentFlow = issueCommentFlow)
}

Le code ci-dessus obtient d'abord les données correspondantes de la base de données. Si les données ne sont pas vides, elles seront mises à jour viewStatepuis initialisées IssueCommentsPagingSource. À ce stade, IssueCommentsPagingSourcecommencera immédiatement à demander des données réseau et si la demande aboutit issueCommentFlow, elle sera mise à jour à et rappellera également onLoadFirstPageSuccess()la fonction, dans cette fonction, nous réinitialisons les données mises en cache à vide pour nous assurer que l'interface utilisateur utilisera issueCommentFlowles données au lieu de continuer à utiliser les données mises en cache.

Enfin, nous l'appellerons ainsi dans le code de l'interface utilisateur :

val commentList = viewState.issueCommentFlow?.collectAsLazyPagingItems()
val cacheList = viewState.cacheCommentList

BasePagingLazyColumn(
   commentList,
   cacheList
) {
    
    
     // ……
}

Résumer

Jusqu'à présent, nous avons implémenté notre propre Stale-while-revalidate .

Le code complet de l'application Github peut être trouvé ici : githubAppByCompose

Cependant, en fait, il y a encore un petit défaut dans le code ici, c'est-à-dire que nous BasePagingLazyColumnn'avons pas utilisé la même source de données lors de la définition de , ce qui fera clignoter le plein écran lorsque la requête réseau sera terminée et que les données seront mises à jour.

Pour ce scintillement, je pense que les développeurs Android le connaissent très bien. Dans le système de vue Android traditionnel, cette situation se produira également lors de la mise à jour des données de la vue de liste telle que RecyclerView, et la façon de résoudre cette situation dans la vue traditionnelle Il s'agit d'actualiser la liste à la demande et d'actualiser uniquement les éléments de liste modifiés.

Donc, ici, notre solution au LazyColumnscintillement de l'écran lorsque les données sont mises à jour dans Compose est naturellement la même, c'est-à-dire que nous devons écrire une classe diff, puis LazyColumnutiliser la seule source de données qu'elle contient. be Actualiser les données modifiées via diff, au lieu de remplacer grossièrement la source de données entière.

Bien sûr, cet article est à portée de main, je ne parlerai donc pas de la mise en œuvre spécifique. Les lecteurs qui en ont besoin sont invités à s'entraîner par eux-mêmes.

Acho que você gosta

Origin blog.csdn.net/sinat_17133389/article/details/131143662
Recomendado
Clasificación