- 전체
- 명
- 오늘 찾아주신 분
- 명
이전 포스팅 에서는 Glide 와 Pdfium 라이브러리를 앱에서 사용하기 위한 방법에 대해 간단하게 알아보았다. 못보고 왔다면 조회수 올리게 한번 보고 오세용
이전 꺼는 그냥 어느 정도 보면 대충 만드는 정도지만 여기서는 직접적인 구현이 들어가서 (영어문서를) 이해하는 데 조금 어려웠다. 그럼 시작해보도록 하자.
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 를 등록해보는 포스팅을 올려보도록 하겠다.
[Android] Web 에서 App 으로 데이터를 받아보자 (0) | 2022.02.04 |
---|---|
[Android] Glide with PDF -3(완결)- (0) | 2022.02.04 |
[Android] Glide with PDF -1(기초공사)- (0) | 2022.01.26 |
[Kotlin] 문자열 배열 한글, 영문, 숫자로 정렬 시키기 (0) | 2022.01.26 |
[Android] Onedrive SDK 적용기 (6) | 2022.01.25 |