Glide

Biblioteki

  |   13 min czytania
(Glide 4.9)

Wstęp

Glide jest wydajną biblioteką służącą do zarządzania multimediami, ich dekodowaniem i buforowaniem w pamięci oraz na dysku w celu ponownego szybkiego użycia. Skupia się przede wszystkim na płynnym i wydajnym wyświetlaniu, pobieraniu i modyfikowaniu obrazów. Ponadto obsługuje także obrazy animowane GIF oraz wideo. Zawiera elastyczne i prosty interfejs API, który umożliwia podłączanie niemal dowolnego klienta sieciowego (domyślnie jest to HttpUrlConnection).

Ładowanie

Pobieranie i ładowanie obrazów jest proste i może ograniczyć się zaledwie do jednego ciągu instrukcji. Wyrażenie with zwraca obiekt typu RequestBuilder na którym należy wskazać źródło (load) i typ zasobu, miejsce docelowe (into) oraz opcjonalnie dokonać konfiguracji żądania. Możliwe jest także pobranie zasobu do wskazanego typu obietku z pominięciem ładowania do kontrolki widoku przez wskazanie jako miejsca docelowego obiektu typu Target. Glide automatycznie wylicza rozmiar obrazu, aby dopasować go do widoku w którym będzie wyświetlany, jednakże jawnie ustawiony rozmiar przyspiesza proces przetwarzania. W trakcie niszczenia komponentu powiązane z nim zasoby są poddawane recyklingowi, a te nieużywany zostają usuwane.

private fun loadImage() {
    //default load source as Drawable
    Glide.with(this) //pass Context - returns RequestBuilder
        .load(url) //pass url, file, drawable object, local resource etc
        //set some RequestListener if needed
        .into(imageView) //tell where to load image
}

private fun loadImageIntoBitmapTarget() {
    val target = object: CustomTarget<Bitmap>() {
        override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
            val bitmap = resource
            //do something with downloaded bitmap
        }
        override fun onLoadCleared(placeholder: Drawable?) {
            //action when loading canceled
        }
        //implement other callback methods like onLoadStarted, onLoadFailed
    }

    //could be: asDrawable, asBitmap, asFile, asGif
    Glide.with(this).asBitmap().load(url).into(target)
}

private fun clearData() {
    //manual loading data clear
    Glide.with(this).clear(imageView) //or pass target
}

Symbol zastępczy

Symbol zastępczy (placeholder) jest graficznym wyświetlanym w trakcie pobierania i przetwarzania obiektu źródłowego. Gdy żądanie zostaje ukończone pozytywnie wówczas placeholder jest zastępowany przez źródło. Ładowanie odbywa się na wątku głównym, a transformacje są niedozwolone. Dodatkowo możliwe jest ustawienie obiektu placeholder dla żądania zakończonego kodem błędu (error i fallback), jednakże gdy nie zostanie on ustawiony wówczas pozostanie wyświetlany bieżący placeholder (jeśli został ustawiony). Ponadto możliwe jest ustawienie miniatury (thumbnail), która jest pobierana równolegle z głównym żądaniem co pozwala na zwiększenie doświadczeń użytkownika przez wyświetlenia obrazu niższej rozdzielczości (zamiast zastępnika) w trakcie oczekiwania na pobranie pełnego obrazu.

private fun loadWithPlaceholders() {
    Glide.with(this).load(url)
        .placeholder(R.drawable.placeholder) //main placeholder
        .error(R.drawable.placeholder_error) //placeholder when request permanently fails
        .fallback(R.drawable.placeholder_fallback) //placeholder when requested model is null
        .into(imageView)
}

private fun loadWithThumbnail() {
    Glide.with(this).load(url)
        .thumbnail(Glide.with(this).load(url_miniature))
        .into(imageView)
}

Opcje

Glide oferuje wiele opcji przetwarzania i ładowania zasobów takich jak m.in. transformacje, przejścia, czy buforowanie które można zastosować dla wybranych żądań bezpośrednio na obiekcie RequestBuilder. Opcje mogą być także współdzielone przez instancję RequestOptions (transformacje i strategie buforowania) oraz TransitionOptions (przejścia). Transformacje (transitions) zwracają zmodyfikowany zasób i są używane przede wszystkim do przycinania obrazu i stosowania filtrów. Zastosowanie metody dowolnej transformacji zastępuje poprzednią dlatego w celu zaaplikowania kilku transformacji należy przekazać je do metody transform. Przejścia (transformations) działa w kontekście pojedynczego żądania i pozwalają zdefiniować w jaki sposób Glide powinien przejść z obiektu zastępczego czy miniatury do załadowanego docelowego obrazu. Ponadto użycie przejść wpływa na wydajność w związku z czym należy rozważyć unikanie animacji przejść szczególnie w przypadku kolekcji.

private fun loadImageCustom() {
    //load image with some custom modifications
    Glide.with(this).load(url)
        .centerCrop() //apply some transforms
        .transition(withCrossFade())
        .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
        .into(imageView)
}

private fun loadImageCustomOptions() {
    //TransitionOptions are tied with specific type like DrawableTransitionOptions
    val transitionOptions = DrawableTransitionOptions()
        .crossFade()

    //create RequestOptions and reuse it
    val requestOptions = RequestOptions()
        .centerCrop()
        .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
        //add more transforms

    Glide.with(this).load(url)
        .transition(transitionOptions)
        .apply(requestOptions).into(imageView)
}

private fun loadImageMultipleTransforms() {
    Glide.with(this).load(url)
        .transform(FitCenter(), Rotate(90))
        .into(imageView)
}

Pamięć

Zanim Glide rozpocznie pobieranie nowego zasobu dokonuje sprawdzenia warstw pamięci w celu jego odnalezienia i ponownego użycia co przebiega w następujących krokach:

1. Aktywne zasoby (obraz jest wyświetlany w innym widoku)
2. Pamięc podręczna (obraz został niedawno załadowany i pozostaje nadal w pamięci)
3. Zasób (obraz został wcześniej zdekodowany, przekształcony i zapisany w pamięci dysku)
4. Dane (dane z których uzyskano obraz zapisano wcześniej w pamięci podręcznej dysku)

Jeśli zasób nie został odnaleziony w żadnej warstwie pamięci wówczas zostaje on pobrany z oryginalnego źródła. Weryfikacja istnienia zasobu w warstwach pamięci odbywa się na podstawie wyszukiwania klucza składającego się z modelu (File, Uri, Url itp.), opcjonalnej sygnatury dołączonej metodą signature oraz parametrów zasobów takich jak m.in. wielkość, transformacje, opcje czy typ (dla kroków 1-3). Glide pozwala na uwzględnienie (onlyRetrieveFromCache) lub pominięcie pamięci podręcznej (skipMemoryCache) oraz dostarcza kilka strategii dla pamięci dysku (DiskCacheStrategy), które umożliwiają wybór sposobu ładowania i zapisywania pobranych zasobów.

private fun loadImageWithCustomSignature() {
    Glide.with(this).load(url)
        .signature(ObjectKey("version")) //custom metadata if possible like last modified time
        .into(imageView)
}
	
private fun loadAndSaveImageBySomeStrategy() {
    Glide.with(this).load(url)
        .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC) //is default
        .into(imageView)

    /* use one of DiskStrategyCache:
    AUTOMATIC - optimal strategy based on data source
    DATA - stores original retrieved data in disk cache
    RESOURCE - stores data in disk cache after decoding
    ALL - remote data with both DATA and RESOURCE, local data only with RESOURCE
    NONE - saves no data to disk cache */
}

private fun loadImageOnylIfExistsInCache() {
    Glide.with(this).load(url)
        .onlyRetrieveFromCache(true)
        .into(imageView)
	
    //if image doesn't exists in memory or disk cache then load fail
}

private fun loadImageSkippingCache() {
    Glide.with(this).load(url)
        .skipMemoryCache(true)
        .into(imageView)
	
    //skip memory cache
}

Wielkość pamięci jest automatycznie ustalana i może być modyfikowana w oparciu o klasę MemorySizeCalculator. Tymczasowe zwiększenie pamięci podręcznej odbywa się za pomocą metody setMemoryCategory, a ręczne czyszczenie pamięci podręcznej i dysku przy użyciu clearMemory (na głównym wątku) oraz clearDiskCache (na wątku pobocznym).

Interfejs API

Glide pozwala na rozszerzenie interfejsu API dzięki czemu możliwe jest ustawienie i nadpisanie domyślnych opcji globalnych dla żądań oraz dołączenie bibliotek integracyjnych. Aby wygenerować interfejs należy stworzyć klasę modułu AppGlideModule opatrzoną adnotacją @GlideModule oraz opcjonalnie klasę rozszerzeń @GlideExtension z metodami statycznymi oznaczonymi jako @GlideOption i @GlideType.

@GlideModule
class CustomAppGlideModule : AppGlideModule() {

    //this will apply as global settings
    override fun applyOptions(context: Context, builder: GlideBuilder) {
        //create custom options
        val requestOptions = RequestOptions()
            .circleCrop()
            .diskCacheStrategy(DiskCacheStrategy.DATA)

        val transitionOptions = DrawableTransitionOptions()
            .crossFade()

        //set custom memory size
        val customMemoryCacheSize = (MemorySizeCalculator.Builder(context)
            .build().memoryCacheSize * 1.1).toLong()

        //apply some custom settings
        builder
            .setDefaultRequestOptions(requestOptions)
            .setDefaultTransitionOptions(Drawable::class.java, transitionOptions)
            .setLogLevel(Log.ERROR)
            .setMemoryCache(LruResourceCache(customMemoryCacheSize))
    }

    //body can be also empty
}

@GlideExtension
object CustomGlideExtension {
    
    //class with private constructor and static annotated methods

    @GlideOption
    @JvmStatic
    fun smallCircle(options: BaseRequestOptions<*>): BaseRequestOptions<*> {
        return options.circleCrop().override(100)
    }

    //more @GlideOption and @GlideType methods
}

Odwołanie do stworzonego modułu odbywa się domyślnie na instancji GlideApp tworzonej przy budowaniu projektu. Nie wyklucza to jednak użycia w standardowy sposób za pomocą instancji Glide.

private fun loadImageByCustomModule() {
    //will draw circle crop 100px image with cross fade transition and custom log and cache settings
    GlideApp.with(this).load(url)
        .smallCircle() //new custom option
        .into(imageView)
}

RecyclerView

Glide ułatwia współpracę z RecyclerView (obrazy są ładowane wcześniej zgodnie z kierunkiem przesuwania) poprzez użycie RecyclerViewPreloader jako obiektu słuchacza dla akcji przesuwania addOnScrollListener. W połączeniu z odpowiednim rozmiarem obrazu i optymalną strategią pamięci pozwala na zmniejszenie liczby ładowanych obrazów dostarczając je wcześniej.

//usage of RecyclerViewPreloader in RecyclerView
class RecyclerViewActivity : AppCompatActivity() {

    private lateinit var recyclerView: RecyclerView
    private lateinit var preloader: RecyclerViewPreloader<Any>
    private val urls = listOf("url1", "url2", "url3") //mock some url

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_recyclerView)

        initPreloader()
        initRecyclerView()
    }

    private fun initPreloader() {
        val size = 100
        val maxPreload = 10
        val sizeProvider: ListPreloader.PreloadSizeProvider<Any> = FixedPreloadSizeProvider(size, size)
        val modelProvider = CustomPreloadModelProvider(urls, this) //own implementation of PreloadModelProvider
        preloader = RecyclerViewPreloader(this, modelProvider, sizeProvider, maxPreload)
    }

    private fun initRecyclerView() {
        recyclerView = findViewById(R.id.recyclerView)
        recyclerView.apply {
            layoutManager = LinearLayoutManager(this@MainActivity)
            adapter = CustomAdapter(urls, this@MainActivity)
            addOnScrollListener(preloader) //set RecyclerViewPreloader as scroll listener
        }
    }
}

//implement PreloadModelProvider
class CustomPreloadModelProvider(val urls: List<String>, val context: Context) : ListPreloader.PreloadModelProvider<Any> {

    override fun getPreloadItems(position: Int): MutableList<Any> {
        val url = urls[position]
        if (TextUtils.isEmpty(url)) {
            return Collections.emptyList()
        }
        return Collections.singletonList(url)
    }

    override fun getPreloadRequestBuilder(item: Any): RequestBuilder<*>? {
        return GlideApp.with(context).load(item)
    }
}

//just simple adapter
class CustomAdapter(val urls: List<String>, val context: Context) : RecyclerView.Adapter<CustomAdapter.CustomViewHolder>() {

    class CustomViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val imageView: ImageView
        init {
            imageView = view.findViewById(R.id.imageView)
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.item_layout, parent, false)
        return CustomViewHolder(view)
    }

    override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
        //set some data on item
        GlideApp.with(context).load(urls[position]).into(holder.imageView) //standard Glide loading
    }

    override fun getItemCount(): Int = urls.size
}