Tempo Di Valse

[Android] Glide with PDF -2(ModelLoader, DataFetcher)- 본문

개발/Android

[Android] Glide with PDF -2(ModelLoader, DataFetcher)-

TempoDiValse 2022. 1. 31. 22:18

이전 포스팅 에서는 Glide 와 Pdfium 라이브러리를 앱에서 사용하기 위한 방법에 대해 간단하게 알아보았다. 못보고 왔다면 조회수 올리게 한번 보고 오세용

 

 

Glide with PDF -1(기초공사)-

Glide 는 Android 에서는 거의 필수적으로 사용하는 이미지 로드 라이브러리이다. 뭐 이보다 Lightweight 하다는 Picasso 도 유명한 라이브러리 이지만, Glide 가 그나마 접근하기 쉬운 라이브러리이며 구

tempodivalse.tistory.com

 


이전 꺼는 그냥 어느 정도 보면 대충 만드는 정도지만 여기서는 직접적인 구현이 들어가서 (영어문서를) 이해하는 데 조금 어려웠다. 그럼 시작해보도록 하자.

 

Glide 문서를 보면 Writing a custom ModelLoader 라는 섹션을 찾아볼 수 있는데, 해당 문서가 너무 잘 되어있어서 순서대로 따라 만들수 있게 되어있다. 완벽한 번역은 나도 네이티브 스피커가 아니기 때문에 부분부분 이해를 할 수 있었지만, 대략의 순서는 다음과 같다

 

1. ModelLoader 를 구현한다

2. DataFetcher 를 구현한다.

3. ModelLoader 에서 쓸 수 있도록 만든다

4. AppGlideModule 에 등록한다.

 

이렇게 4개의 섹션으로 되어있지만 막상 구현하라고 하면 어떻게 할 지 모르는데 다행히도 Base64 인코딩 된 데이터를 ModelLoader, DataFetcher 로 만드는 예시가 있어서 그것을 참고하여 만들 수 있었다. 하지만 우리는 PDF 관련 Loader 를 구현해야 하니까 하나씩 공식문서를 토대로 만들어 보도록 하자.

 

1. ModelLoader 구현

Glide 문서에 따르면 ModelLoader 에서 다룰 수 있는 스트림 객체는 InputStream 과 ByteBuffer, ParcelFileDescriptor 세가지를 기본적으로 지원하고 있다고 말하고 있는 "것 같다." 여기서 ParcelFileDescriptor 는 동영상 객체의 Frame 을 렌더시키기 위해 지원 해주고 있다고 얘기 한다.

 

이 중에서 나는 ByteBuffer 를 전달하여 로드 해보려 한다. 왜냐면 가이드 문서에도 ByteBuffer 를 사용하고 있기 때문이다. 로드할 때 사용하는 Model 객체는 Uri, File, String 등등.. 사용할 수 있지만, 여기서는 Uri 를 이용해보돍 한다.

 

ModelLoader 는 Generic 이기 때문에 이를 상속하게 되면 첫번째 타입은 Model 의 객체가 될 것이고, 두번째 타입은 변환할 Data 타입이 될 것이다. 그래서 ModelLoader <Uri, ByteBuffer> 로 해서 상속을 받게 된다면 다음과 같은 메소드들이 자동 입력된다.

 

사진 내에도 주석으로 적어놓았지만,

 

- buildLoadData

- handles

 

이 두가지 메소드들을 오버라이드 하도록 입력해준다.

 

먼저 가장 알기 쉬운 handles() 메소드를 알아보자면, 객체들 중에서 어떤 객체를 이 ModelLoader 에서 다룰 것인지 필터링 해주는 메소드라고 할 수 있다. 우리는 Uri 를 이용하여 PDF 객체를 다룰 것이기 context 의 contentResolver 를 이용하여 mimeType을 가져올 것이다.

override fun handles(model: Uri): Boolean 
    = context.contentResolver.getType(uri) == "application/pdf"
 

만약 File 객체를 사용하려 한다면,

override fun handles(model: File): Boolean = model.extension == "pdf"
 

webkit 의 MimeTypeMap 을 사용해서 판별할 수 있을 것이다.

override fun handles(model: File): Boolean 
    = MimeTypeMap.getSingleton().getMimeTypeFromExtension(model.extension) == "application/pdf"
 

 

다음으로 buildLoadData 는, DataFetcher 에서 가져온 객체를 ModelLoader.LoadData 형태로 전달해줘야 한다. 여기에서 ModelLoader.LoadData 의 Constructor 형태는 Cache Key 그리고 Fetcher 순서로 Parameter 를 받고 있다. 그래서 리턴하는 형태를 간략히 보면,

override fun buildLoadData(model: Uri, width: Int, height: Int, options: Options) 
: ModelLoader.LoadData<ByteBuffer>? { 
    return ModelLoader.LoadData<ByteBuffer>(CACHE_KEY, FETCHER) 
}
 

이런 식으로 받아야 하기 때문에 DataFetcher 가 구현이 먼저 선행되어야 가능하다.

 

2. DataFetcher 구현

DataFetcher 에서는 실질적으로 Uri 에서 추출한 데이터를 ByteBuffer 로 변환하여 내보내는 역할을 하는 클래스 이다. 

DataFetcher 도 Generic 형태인데 파라미터는 ModelLoader 로 리턴할 데이터 형태를 넣어주면 된다. 그러니까 ModelLoader Generic 의 두번째 타입을 넣어주면 된다는 것이다. 그리고나서 override 할 것들을 자동으로 불러오면 총 5개의 메소드를 override 하게 된다.

 

- loadData

- cleanup

- cancel

- getDataClass

- getDataSource

 

먼저 제일 하단의 getDataSource() 는, DataSource 의 Cache Policy 에 대해 정의를 하는 것 같은데 Preset 되어있어서 크게 구현하지 않아도 된다. 어떤 목적으로 사용하는 지는 잘 모르지만, Local 데이터를 불러오는 것에는 Local 로 맞추고 Http 에서 불러오는 것에서는 Remote 로 맞추라는 공식문서의 설명이 있기에 해당 메소드는 다음과 같이 맞추도록 한다.

override fun getDataSource(): DataSource = DataSource.LOCAL
 

그 외에도 DataSource 에는 MEMORY_CACHE, DATA_DISK_CACHE, RESOURCE_DISK_CACHE 등이 있는데 이것들은 어떤 역할을 하는 지 정확하게 파악하지 못했으니 파악하면 수정하는걸로.

 

다음, getDataClass() 는 단순하게 리턴하는 타입의 클래스객체를 리턴해주면 된다. 그래서

override fun getDataClass(): Class<ByteBuffer> = ByteBuffer::class.java
 

이렇게만 해주면 끝난다

 

다음, cancel() 은 Http 요청을 하는 모델인 경우에만 사용한다고 나와있고, cancellation 하는 API 가 있는 경우에 해당 메소드에서 호출하도록 구현하라고 써있다. 그래서 나는 해당 구역은 패스하도록 했다.

 

다음, cleanup() 은 I/O 가 일어난 모델인 경우에는 해당 메소드안에서 release 하는 방식을 구현하라고 문서에는 나와있다. 이 부분은 loadData와 함께 구현하는 것이 맞기 때문에 잠시 Pending.

 

마지막, loadData() 이 메소드에서 Uri 를 통해 데이터를 추출하여 ByteBuffer 로 담아내는 일을 하게 된다. 그렇다는 것은 PDF 를 로드하는 것을 이 부분에서 해야 한다는 이야기가 되는 것이다. 

 

PDF 를 로드하는 라이브러리에 대해서는 이전에 Pdfium 을 사용하면 되는데, Pdfium 은 FileDescriptor 객체를 이용하여 PDF 를 네이티브 소스에서 열게 된다.

 

이래저래 하고 여차저차 하게 되어 ByteBuffer 로 데이터를 변환할 수 있게 되었다면 loadData 의 callback 객체 내에 있는 onDataReady() 메소드에 만들어진 ByteBuffer 를 던진다. 하지만 혹시나 PDF 처리를 하다가 에러가 발생할 수도 있다. 그런 경우를 위해 callback 객체 내에는 onLoadFailed() 라는 객체를 만들어 놓았다. 

 

그래서 간략한 형태를 보면,

override fun loadData(priority: Priority, callback
    : DataFetcher.DataCallback<in ByteBuffer>) {
    try {
        /* 신나게 코드를 짠다. 막 짠다. */
        callback.onDataReady(BYTE_BUFFER)
    } catch (e : Exception) {
        callback.onLoadFailed(e)
    }
}
 

이런 식으로 분기처리 하도록 한다. 해당 callback 은 Glide 의 Chain 메소드인 addListener() 의 object 에 들어가게 된다.

 

DataFetcher 까지 만들었다면 다시 ModelLoader 로 돌아가 미처 구현하지 못한 곳을 구현해본다.

 

3. ModelLoader 에서 쓸 수 있도록 만든다

ModelLoader 부분에서 언급한 내용을 보면, 

다음으로 buildLoadData 는, DataFetcher 에서 가져온 객체를 ModelLoader.LoadData 형태로 전달해줘야 한다. 여기에서 ModelLoader.LoadData 의 Constructor 형태는 Cache Key 그리고 Fetcher 순서로 Parameter 를 받고 있다.

라고 했는데, 이제 Fetcher 객체는 DataFetcher 가 구현되었으니 완성이 되었고 Cache Key 를 만들어 볼 차례이다. 

 

Glide 에서 다뤄지는 Cache Key 는 Key 라고 하는 Interface 를 상속받아 구현하는데, 따로 그럴 필요도 없이 Glide 에서 제공하는 ObjectKey 객체를 이용하여 만들면 된다. 그래서,

ObjectKey("PAGE_${model}_")
 

이런 식으로 만들면 되는데, 이것을 바로 fetcher 객체와 묶어 보내면 문제되는 것이 있다. 바로 Page 별로 공통 Cache Key 가 생성된다는 것이다. 그래도 페이지 별로 Caching 이 먹혀야 Glide 를 쓰는 목적이 맞는 건데 Cache Target 이 똑같아 버리면 페이지를 여러 개 불러온다 해도 모든 View 에서는 마지막 PDF 페이지만 불러오게 되는 대참사를 겪게 되기 때문에 Page 별로 다른 Key 를 가지도록 만들어야 한다. 

 

그래서 CacheKey 는, 

ObjectKey("PAGE_${model}_${page}")
 

이렇게 되어야 하는 것이 맞는 것이다. 그런데, 정작 메소드 내에서는 page 정보를 가져올 방법이 없다는 것이다. 그러면 어떻게 하느냐.. 해서 메소드의 파라미터를 보면 Options 객체가 있는 것이다. 우리는 이 Options 객체를 가지고 Page 정보를 가져올 수 있도록 로직을 만들 것이다.

 

Option 객체는 Glide 객체 내에서 유일한 키 값을 가지고 있어야 하고, static 의 영역에서 선언이 되어야 하는 조건이 있다. 그래서 Kotlin 에서의 Static 영역인 Companion 에 Option 을 선언해 주도록 한다.

companion object {
    private const val OPTION_KEY_PAGE = "glide.pdf.page"
    val OPTION_PAGE : Option<Int> = Option.memory(OPTION_KEY_PAGE, 0)
}
 

Key 값은 Unique 하게 지어주면 되기 떄문에 아무렇게나 식별 가능하게만 지어준다. 그렇다고 저걸 이용하여 Value 를 뽑아내는 방식이 아닌 다른 방식을 사용하기 때문에 그냥 지정만 해놓는다. 왜냐하면 Option<*> 으로 선언된 객체 자체가 Key 가 되기 때문이다.

 

Option 객체에는 memory(), disk() 두 가지의 정의 방법이 있는데 둘의 차이점은 해석이 모호한 관계로 그냥 감만 잡아 개발 하였고, disk cache 를 쓰지 않는 조건으로 개발 했기 때문에 memory() 메소드를 이용하여 간단하게 개발했다. 

 

만약 disk() 메소드를 이용하여 개발하게 된다면 다음과 같이 복잡한 개발이 이뤄진다. 처음에는 이 방식으로 개발 해봤는데 해당 내용은 Glide 에서 사용하고 있는 방식을 보고 바꾼 내용이라 무슨 작동 방식인 지는 잘 이해되지 않는다.

val OPTION_PAGE : Option<Int> = Option.disk(OPTION_KEY_PAGE, 0, object : 
    Option.CacheKeyUpdater<Int> {
    private val buffer = ByteBuffer.allocate(Int.SIZE_BITS / Byte.SIZE_BITS)

    override fun update(keyBytes: ByteArray, value: Int, messageDigest: MessageDigest) {
        val _v = value.coerceAtLeast(0)

        messageDigest.update(keyBytes)

        synchronized(buffer){
            buffer.position(0)
            messageDigest.update(buffer.putInt(_v).array())
        }
    }
})
 

대략 Buffer 할당해서 Buffer 내용을 disk 에 할당할 것 같은 분위기였다. 이번 개발에서는 memory() 방식으로 했는데 이것도 잘 되가지고 간단한 것으로 해보도록 했다. 이제 옵션도 정의 했으니, Glide 객체에 Option 을 달아보도록 하자. 

 

Glide 는 Chaining 패턴을 가지고 있기 때문에 이것을 유지하기 위해서는 Kotlin 의 extension 기능을 사용하여 추가를 해보았다. 그래서

/* GlideExtensions.kt */

fun GlideRequest<Bitmap>.page(page: Int) = apply {
    set(PDFModelLoader.OPTION_PAGE, page)
}
 

extension 파일만 만들어놓고 Global 하게 사용할 수 있도록 만들어놓았고, Chaining 이 끊기지 않도록 GlideRequest 를 이용하도록 했다. 어차피 Bitmap 을 가져올 거라 Bitmap 으로 타입을 박아놓았다.

 

그리고 ModelLoader 에도 Page 를 받을 수 있도록 변경한다.

override fun buildLoadData(model: Uri, width: Int, height: Int, options: Options)
    : ModelLoader.LoadData<ByteBuffer>? {
    val page = options[OPTION_PAGE]
    val key = ObjectKey("PAGE_${model}_${page}")

    /* Data Fetcher 를 정의해 본다 */

    return ModelLoader.LoadData(key, DATA_FETCHER)
}
 

그러고 나서 구현한 DataFetcher에 Page 를 받을 수 있도록 하면 각 페이지 별로 렌더를 시킬 수 있고, Cache Key 까지 각 페이지별로 발급을 해줄 수 있게 되는 것이다.


다음 포스팅에서는 Glide 에 지금까지 만들어놓은 ModelLoader 를 등록해보는 포스팅을 올려보도록 하겠다.

 

반응형
Comments