5分鐘學會設計模式之策略模式(圖片載入框架)

語言: CN / TW / HK

1、前言

系列文章

5分鐘學會設計模式之策略模式(Strategy Pattern)

上一篇,我們舉例一個View的例子,可能不是很清晰,所以根據公司用的圖片載入Glide來封裝一個圖片載入框架,非常的簡潔易懂~

有時候Glide已經不能完全滿足需求,或者出現了一些效能或穩定性問題。這時候,如果我們在程式碼中直接使用了特定的框架,那麼我們需要對整個應用程式進行修改,這將會帶來很大的風險和困難。對於圖片載入框架來說,我們需要封裝一些抽象的介面,使得我們可以在不影響應用程式其他部分的情況下,輕鬆更換底層的圖片載入庫。同時,我們還需要將圖片載入的邏輯封裝在一個獨立的模組中,使得我們可以單獨對其進行更新和維護。

如果您有任何疑問、對文章寫的不滿意、發現錯誤或者有更好的方法,歡迎在評論、私信或郵件中提出,非常感謝您的支援。🙏

2、抽象行為(策略介面)

IImageEngine介面就是策略介面,定義了載入圖片的三個方法:loadResource、loadUrl和loadBitmap。它抽象出了不同的圖片載入庫所需實現的核心功能,併為具體策略提供了統一的方法簽名。

interface IImageEngine {    /**     * 給定的資源 ID 載入一個圖片資源     */    fun loadResource(        view: ImageView?,        @DrawableRes resourceId: Int,        radius: Int = 0,        callback: ImageLoadCallback?   )    /**     * 指定的 URL 載入一張圖片     */    fun loadUrl(view: ImageView?, url: String, radius: Int = 0, callback: ImageLoadCallback?) ​    /**     * 從指定的 URL 載入一張圖片,並返回一個 Bitmap 物件。     */    fun loadBitmap(context: Context, url: String, radius: Int = 0, callback: ImageLoadCallback?) }

3、載入回撥

當然,所有的載入,不會總是一帆風順,所以我們需要一個載入回撥

interface ImageLoadCallback {    fun onSuccess(resource: Bitmap?) {}    fun onLoadStarted(placeholder: Drawable?) {}    fun onLoadFailed(errorDrawable: Drawable?) {} }

4、GlideEngine實現

GlideEngine是實現了IImageEngine介面的具體實現類,我們通過閱讀程式碼就可以得知

internal object GlideEngine : IImageEngine {    override fun loadResource(        view: ImageView?,        resourceId: Int,        radius: Int,        callback: ImageLoadCallback?   ) {        view?.run {            Glide.with(this)               .load(resourceId).also {                    if (radius > 0) {                        it.apply(RequestOptions().transform(RoundedCorners(radius)))                   }               }.into(createTarget(view, callback))       }   } ​    override fun loadUrl(        view: ImageView?,        url: String,        radius: Int,        callback: ImageLoadCallback?   ) {        view?.run {            Glide.with(context).load(url).also {                if (radius > 0) {                    it.apply(RequestOptions().transform(RoundedCorners(radius)))               }           }.into(createTarget(view, callback))       }   } ​    override fun loadBitmap(        context: Context,        url: String, radius: Int,        callback: ImageLoadCallback?   ) {        Glide.with(context).asBitmap().load(url).also {            if (radius > 0) {                it.apply(RequestOptions().transform(RoundedCorners(radius)))           }       }.into(createBitmapTarget(callback))   } ​    private fun createTarget(        view: ImageView?,        callback: ImageLoadCallback?   ): CustomTarget<Drawable> {        return object : CustomTarget<Drawable>() {            override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {                view?.setImageDrawable(resource)                callback?.onSuccess(null)           } ​            override fun onLoadStarted(placeholder: Drawable?) {                callback?.onLoadStarted(placeholder)           } ​            override fun onLoadFailed(errorDrawable: Drawable?) {                callback?.onLoadFailed(errorDrawable)           } ​            override fun onLoadCleared(placeholder: Drawable?) {}       }   } ​    private fun createBitmapTarget(callback: ImageLoadCallback?): CustomTarget<Bitmap> {        return object : CustomTarget<Bitmap>() {            override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {                callback?.onSuccess(resource)           }            override fun onLoadStarted(placeholder: Drawable?) {                callback?.onLoadStarted(placeholder)           }            override fun onLoadFailed(errorDrawable: Drawable?) {                callback?.onLoadFailed(errorDrawable)           }            override fun onLoadCleared(placeholder: Drawable?) {}       }   } }

5、COilEngine實現

GlideEngine是實現了IImageEngine介面的具體實現類,我們通過閱讀程式碼就可以得知

​ class COilEngine : IImageEngine { ​    override fun loadResource(        view: ImageView?,        @DrawableRes resourceId: Int,        radius: Int,        callback: ImageLoadCallback?   ) {        view?.setImageResource(resourceId)        callback?.onSuccess(null)   } ​    override fun loadUrl(        view: ImageView?,        url: String,        radius: Int,        callback: ImageLoadCallback?   ) {        view?.let { imageView ->            val request = createImageRequest(url, radius, callback)            imageLoader().execute(request) {                it.drawable?.let { drawable ->                    imageView.setImageDrawable(drawable)                    callback?.onSuccess(null)               } ?: run {                    callback?.onLoadFailed(null)               }           }       }   } ​    override fun loadBitmap(        context: Context,        url: String,        radius: Int,        callback: ImageLoadCallback?   ) {        val request = createImageRequest(url, radius, callback)        imageLoader().execute(request) {            it.toBitmap()?.let { bitmap ->                callback?.onSuccess(bitmap)           } ?: run {                callback?.onLoadFailed(null)           }       }   } ​    private fun createImageRequest(        url: String,        radius: Int,        callback: ImageLoadCallback?   ): ImageRequest {        val builder = ImageRequest.Builder(context)           .data(url)           .parameters(Parameters.Builder().set("radius", radius).build()) ​        if (callback != null) {            builder.target(object : Target {                override fun onStart(placeholder: Drawable?) {                    callback.onLoadStarted(placeholder)               }                override fun onError(error: Drawable?) {                    callback.onLoadFailed(error)               }                override fun onSuccess(result: Drawable) {} ​                override fun onCancel() {}           })       }        return builder.build()   } ​    private fun imageLoader(): coil.ImageLoader {        return Coil.loader()   } }

6、上下文類的定義

ImageLoader類則是上下文類,負責根據呼叫方的需要,選擇不同的圖片載入庫。它把具體的策略實現作為一個引數,通過建構函式或者其他方式注入到類中。在執行圖片載入時,ImageLoader物件呼叫傳入的具體策略實現的方法。

一般而言,我們會這麼寫,通過一個單例,來呼叫傳入的IImageEngine實現類

object ImageLoader {    private var imageEngine: IImageEngine? = null    fun <T : IImageEngine> init(ie: T) {        imageEngine = ie   } ​    fun loadImage(        view: ImageView?,        url: String,        radius: Int = 0,        callback: ImageLoadCallback? = null   ) {        imageEngine?.loadUrl(view, url, radius, callback)   } ​    fun loadResource(        view: ImageView?,        resourceId: Int,        radius: Int = 0,        callback: ImageLoadCallback? = null   ) {        imageEngine?.loadResource(view, resourceId, radius, callback)   } ​    fun loadBitmap(        context: Context,        url: String,        radius: Int = 0,        callback: ImageLoadCallback? = null   ) {        imageEngine?.loadBitmap(context, url, radius, callback)   } }

但是得益於Kotlin的語法糖,我們可以用委託的方法,省略許多的程式碼細節

object ImageLoader : IImageEngine by GlideEngine

其實編譯出來的程式碼是一模一樣的

image-20230219164327319

你也可以皮一點

object ImageLoader1 : IImageEngine by GlideEngine object ImageLoader2 : IImageEngine by CoilEngine

這樣呼叫也是非常簡單了

ImageLoader.loadUrl(imageView, url, radius, callback)

7、擴充套件函式

當然一個懶狗,是不會想多傳一個ImageView物件的,用上Kotlin語法糖

fun ImageView.loadUrl(url: String, radius: Int = 0, callback: ImageLoadCallback?) {    ImageLoader.loadUrl(this, url, radius, callback) } fun ImageView.loadRes(@DrawableRes resourceId: Int, radius: Int = 0, callback: ImageLoadCallback?) {    ImageLoader.loadResource(this, resourceId, radius, callback) }

這樣你就可以直接把它當做ImageView的方法使用了

imageView.loadRes(xxx)

8、總結

作為一個載入圖片的框架,我們需要考慮以下幾點:

  1. 圖片載入的來源:本地圖片、網路圖片、Bitmap 等。
  2. 載入圖片的方式:同步載入、非同步載入,圖片壓縮等。
  3. 圖片載入的效能和效率:需要考慮記憶體佔用、載入速度、快取等。
  4. 對於不同的載入場景,如列表、詳情頁等,需要採用不同的載入策略。

到這兒我們都只考慮第一點的第一部分,不過因為主要是為了講策略模式的應用,所以嘻嘻嘻。不過還是實現了對不同圖片載入庫的無縫切換

對於Glide、Coil的使用不做贅述啦

就到這)-(

如果您有任何疑問、對文章寫的不滿意、發現錯誤或者有更好的方法,歡迎在評論、私信或郵件中提出,非常感謝您的支援。🙏

“開啟掘金成長之旅!這是我參與「掘金日新計劃 · 2 月更文挑戰」的第 5 天,點選檢視活動詳情