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 :
- Ajoutez des animations de chargement, des effets de transition, etc.
- 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 SwipeRefresh
la coopération LazyColumn
et paging3
.
Remarque :
SwipeRefresh
estaccompanist
l'une des bibliothèques du projet, qui a été marquée comme obsolète
L'effet est à peu près le suivant :
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' paging3
implémenter des données de chargement de page.
En fait, dans paging3
la 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 room
pour réaliser la mise en cache des données locales ( RemoteMediator
) :
RemoteMediator
Lorsque les données sont nécessaires, les données seront paging3
interrogé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 paging3
la base de données locale est toujours utilisée comme seule source de données :
Une
RemoteMediator
implé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. UnePagingSource
implé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 RemoteMediator
garantir 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 paging3
de 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é, RemoteMediator
il 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 :
pagingItems
Autrement dit, les dernières données chargées à partir du serveur renvoyées par la paginationcacheItems
c'est-à-dire des données mises en cache localementitemUi
Autrement dit, l'interface utilisateur à afficher
Ensuite, tant que cacheItems
n'est pas vide, nous afficherons cacheItems
les données en premier, et cacheItems
n'afficherons les données que si est vide pagingItems
.
Après cela, nous devons implémenter paging
, PagingSource
ici 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 viewState
puis initialisées IssueCommentsPagingSource
. À ce stade, IssueCommentsPagingSource
commencera 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 issueCommentFlow
les 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 BasePagingLazyColumn
n'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 LazyColumn
scintillement 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 LazyColumn
utiliser 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.