Published by

Il y a 3 semaines -

Temps de lecture 9 minutes

Glide for Android : comprendre et gérer le cache des images

Glide: Kezako

Glide est une bibliothèque de chargement d’images pour Android. Il s’agit de l’un des outils les plus répandus sur le marché avec Picasso, Fresco et Coil qui commence à gagner en visibilité. L’intérêt de Glide est de pouvoir afficher assez facilement des images et GIFs, tout en utilisant si besoin une couche réseau personnalisée.

Vous connaissez certainement les différentes manières de charger des images, que ce soit via des URLs ou des images déjà encodées en base64. L’utilisation basique de l’API se résume en effet à :

Glide.with(fragment) //view ou context
    .load(url) 
    .into(imageView) //id de la Target

Il serait pourtant trop simple de résumer Glide à ces trois lignes.

Les bases

Imaginons une application contenant plusieurs écrans dans lesquels vous affichez des images. Imaginons également une contrainte qui nécessite l’utilisation d’entêtes HTTP personnalisés et qu’il faille appliquer un découpage circulaire sur chaque image. On pourrait faire une duplication de code et utiliser quelque chose de similaire à ceci partout dans l’application :

val glideUrl = GlideUrl(
    "https:my-url.com/photo1",
    Headers { mutableMapOf("My Custom Header Authorization" to "Bearer my-secret-token") }
)

Glide.with(itemView)
    .load(glideUrl)
    .apply(RequestOptions().circleCrop()) 
    .into(target)

Pour éviter cette méthode fastidieuse, l’idéal est de mutualiser ce code dans un GlideModule. Le GlideModule est une class que l’on doit créer à la racine de l’application et qui implémente AppGlideModule.

La suite de cet article suppose que la bibliothèque est installée dans le projet, y compris la partie annotations (si besoin, se référer à la documentation d’installation).

En utilisant les annotations, les paramètres indiqués dans cette CustomGlideApplication seront réutilisés à chaque appel pour afficher une image et permettra donc de mutualiser toutes les options :

@GlideModule
class CustomGlideApplication : AppGlideModule() {

    @Inject lateinit var okHttpClient: OkHttpClient

    override fun registerComponents(context: Context, glide: Glide, registry: Registry) {

        registry.replace(
            GlideUrl::class.java,
            InputStream::class.java,
            OkHttpUrlLoader.Factory(okHttpClient)
        )
    }

    override fun applyOptions(context: Context, builder: GlideBuilder) {
        val requestOptions = RequestOptions().circleCrop()
        builder.setDefaultRequestOptions(requestOptions)
    }
}

Dans le détail, le GlideModule est préparé de manière à ce que lorsque l’on appelle des GlideUrl (wrapper pour requêtes HTTP/HTTPS), la couche réseau soit régentée par l’instance okHttpClient :

registry.replace(
            GlideUrl::class.java,
            InputStream::class.java,
            OkHttpUrlLoader.Factory(okHttpClient)
        )

De la même manière, dans cet exemple, toutes les images téléchargées subiront une transformation de type circleCrop :

 val requestOptions = RequestOptions().circleCrop()
 builder.setDefaultRequestOptions(requestOptions)

Le cache

Nous savons à présent comment effectuer des requêtes relativement simples pour afficher des images, mais nous gâchons une partie de nos performances réseau et mémoire. En effet, si l’on appelle plusieurs fois la même image sur des écrans différents de l’application, il est dommage de refaire un appel réseau à chaque fois. C’est là où le cache de Glide intervient.

Glide peut utiliser différentes stratégies de stockage des images/GIFs: AUTOMATIC (par défaut), ALL, DATA, NONE et RESOURCE. Vous trouverez le détail de chaque stratégie dans la documentation. On peut également, si on le souhaite, charger uniquement les images depuis le cache ou bien pas du tout grâce aux méthodes onlyRetrieveFromCache() et skipMemoryCache().

Le cache (enregistrement, stockage et réutilisation des ressources) est géré par défaut par la bibliothèque, qui génère elle-même les clés nécessaires au stockage et à la récupération des images (cache keys). Ces clés contiennent au moins deux éléments, parmi lesquels le modèle d’objet chargé (File, Uri, Url), une éventuelle Signature, ainsi qu’un savant mélange selon les stratégies entre les dimensions des images, les éventuelles transformations et/ou options appliquées, ainsi que le type de données récupérées (bitmap, GIF, autre). Pour générer le nom de la clé, tous ces éléments sont hachés pour n’obtenir plus qu’un String qui servira de nom de fichier.

Et nous avons besoin d’aller plus loin ?

C’est la partie la plus intéressante de cet article puisque jusqu’à maintenant, nous sommes pour l’instant restés uniquement à la surface de l’API.

Imaginons un cas où l’on a une image à afficher sur plusieurs écrans différents. Selon les cas, elle est communiquée par l’API à l’application soit par sa valeur en base64, soit par son URL. On se souvient que Glide gère lui-même le nommage de ses fichiers en local. La bibliothèque est donc dans l’incacapacité de faire le lien entre l’URL et l’image stockée en base64. Or, l’intérêt d’avoir un outil spécifiquement pour gérer un cache d’images et optimiser les données est bien évidement d’être capable de faire ce lien.

ModelLoader et DataFetcher

Deux interfaces, mais encore ?

Ce sont les deux interfaces qui vont permettre de résoudre cette problématique. Comme l’indique la Javadoc, le ModelLoader et le DataFetcher vont de pair. Le DataFetcher va être utilisé pour indiquer comment récupérer la donnée, et le ModelLoader comment la stocker.

Nous allons réaliser un petit projet dans lequel nous allons exploiter la gestion du cache et faire ce lien entre plusieurs mêmes images. L’objectif va être de précharger tous les logos des différentes versions d’Android au cours des années et de les afficher ensuite, tout en ne les téléchargeant qu’une seule fois.

Nous allons avoir besoin de 2 objets qui vont représenter la même version Android et son logo:

data class AndroidLogo(val apiVersion: String, val url: String)

et

data class JsonAndroidLogo(val apiVersion: String, val image64: String)

Nous allons devoir indiquer à Glide que pour vérifier dans le cache si l’un de nos objets a déjà été téléchargé, il doit se référer à une signature que nous avons nous-même choisie.

Charger les données

Le ModelLoader contient deux méthodes à surcharger :

  • buildLoadData : renvoie un ModelLoader.LoadData contenant un DataFetcher requis pour décoder la donnée fournie en entrée
  • handles : renvoie true si le type de donnée fourni en entrée est reconnu et enregistrable.

Dans le cas de l’objet JsonAndroidLogo, il faut enregistrer une image en base64 et la lier à une version d’API Android. La cache key va donc être forcée de manière à savoir si le logo lié à cet id est déjà présent dans le cache ou pas, et s’il y a besoin de re-télécharger l’image, ou pas.

Pour forcer les cache keys, un objet de type Key doit être utilisé. Dans l’exemple, il s’agit de ClientIdSignature. Il est nécessaire de surcharger la méthode updateDiskCacheKey car c’est ce qui va permettre de savoir si l’image est déjà enregistrée dans le cache ou non.

data class AndroidApiSignature(private val api: String) : Key {
    
    override fun updateDiskCacheKey(messageDigest: MessageDigest) {
        messageDigest.update(api.toByteArray())
    }
}

Il reste donc à créer le DataFetcher correspondant :

class JsonAndroidLogoDataFetcher(private val model: JsonAndroidLogo) : DataFetcher<ByteBuffer> {

    override fun loadData(
        priority: Priority,
        callback: DataFetcher.DataCallback<in ByteBuffer>
    ) {
        val data: ByteArray = Base64.decode(model.image64, Base64.DEFAULT)
        val byteBuffer = ByteBuffer.wrap(data)
        callback.onDataReady(byteBuffer)
    }

    override fun cleanup() {}
    override fun cancel() {}

    override fun getDataClass(): Class<ByteBuffer> {
        return ByteBuffer::class.java
    }

    override fun getDataSource(): DataSource {
        return DataSource.REMOTE
    }
}

Puis à le lier au ModelLoader :

class JsonAndroidLogoModelLoader : ModelLoader<JsonAndroidLogo, ByteBuffer> {

    override fun buildLoadData(
        model: JsonAndroidLogo,
        width: Int,
        height: Int,
        options: Options
    ): ModelLoader.LoadData<ByteBuffer> {
        return ModelLoader.LoadData(AndroidApiSignature(model.apiVersion), JsonAndroidLogoDataFetcher(model))
    }

    override fun handles(model: JsonAndroidLogo): Boolean {
        return !model.image64.isBlank()
    }
}

Maintenant que le ModelLoader est fonctionnel, il faut néanmoins le relier au GlideModule de l’application. Une ModelLoaderFactory va pour cela être utilisée, récapitulant le type de donnée d’entrée, de sortie et le ModelLoader utilisé :

class JsonAndroidVersionModelLoaderFactory : ModelLoaderFactory<JsonAndroidLogo, ByteBuffer> {
    override fun build(unused: MultiModelLoaderFactory): ModelLoader<JsonAndroidLogo, ByteBuffer> {
        return JsonAndroidLogoModelLoader()
    }

    override fun teardown() { // Do nothing.
    }
}

Ce ModelLoaderFactory peut à présent être ajouté au Registry du GlideModule pour être totalement intégré à l’application:

 override fun registerComponents(context: Context, glide: Glide, registry: Registry) {

        registry.prepend(
            JsonAndroidLogo::class.java,
            ByteBuffer::class.java,
            JsonAndroidVersionModelLoaderFactory()
        )

Une fois toutes ces étapes terminées, nous pouvons vérifier avec notre préchargement que lorsque je télécharge un objet JsonAndroidLogo, il est bien ajouté à mon cache :

//androidLogos étant de type List<JsonAndroidLogo>
androidLogos.forEach { 
  GlideApp.with(this).load(it).preload()
}

Attention cependant, à partir de maintenant nous n’allons plus demander à l’application de charger via la bibliothèque une URL ou une base64, mais bien un objet AndroidLogo ou un objet JsonAndroidLogo.

Il ne reste donc plus qu’à faire la même opération avec le AndroidLogo, qui utilise lui une URL basique. Glide fournit dans son API plusieurs types de DataFetchers que l’on peut utiliser à notre convenance. Si une intégration à d’autres API est nécessaire (OkHttp ou autres), des bibliothèques en plus peuvent être disponibles (https://bumptech.github.io/glide/int/about.html)

Le ModelLoader de notre AndroidLogo ressemblera donc à ceci :

class AndroidLogoModelLoader : ModelLoader<AndroidLogo, InputStream> {

    override fun buildLoadData(
        model: AndroidLogo,
        width: Int,
        height: Int,
        options: Options
    ): ModelLoader.LoadData<InputStream> {
        return ModelLoader.LoadData(
            AndroidApiSignature(model.apiVersion),
            HttpUrlFetcher(GlideUrl(model.url), 500)
        )
    }

    override fun handles(model: AndroidLogo): Boolean {
        return true
    }
}

class AndroidLogoModelLoaderFactory : ModelLoaderFactory<AndroidLogo, InputStream> {
    override fun build(unused: MultiModelLoaderFactory): ModelLoader<AndroidLogo, InputStream> {
        return AndroidLogoModelLoader()
    }

    override fun teardown() { // Do nothing.
    }
}

L’AndroidLogoModelLoaderFactory devra être ajoutée de la même manière au Registry du GlideModule et l’objectif sera rempli : si l’objet AndroidLogo est déjà chargé dans le cache de Glide via l’URL, l’objet JsonAndroidLogo ne sera pas rechargé en base64 et vice versa. Ceci peut être vérifié, puisque lorsque nous voulons afficher la liste d’images après le préchargement, nous forçons le fait de ne pas utilier d’autres resources que le cache et nous avons bien notre résultat graphique:

 Glide.with(itemView)
            .load(item)
            .onlyRetrieveFromCache(true)
            .into(itemView.findViewById(R.id.image))


Vous pourrez retrouver le code ayant servi à cet article sur Github: https://github.com/cdreyfus/GitCacheSample .

Glide ne se limite pas à simplement afficher des images : l’utilisation des ressources (cache, appels réseaux) est anticipé et fait partie des nombreuses améliorations à faire au niveau de la plupart des applications Android

Sources:

Published by

Commentaire

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Sapient, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.