위 영상은 테스트용 랜덤 해상도 이미지 1000장을 로드하는 결과이다.
커스텀 갤러리 화면을 구현하고자 한다
기기 내의 대량의 이미지가 있다면 쿼리 실행 및 결과 로드가 늦어질 수 있다.
이를 위해 Paging3를 이용해 구현하기로 한다
구현 해야할 것들은 다음과 같다
이 외에 기본 사용 라이브러리로는 Hilt
, Glide
, ViewBinding
정도가 있다.
먼저 필요한 권한을 manifest에 등록한다.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
...
<uses-permission-sdk-23 android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<application>
...
</application>
</manifest>
그리고 권한 획득을 위한 코드 작성
@AndroidEntryPoint
class GalleryActivity : BaseActivity<ActivityGalleryBinding>(R.layout.activity_gallery) {
private val requiredPermissions = arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE)
...
private fun fetchGalleryAdapter(){
if (hasAllPermissions()) {
// TODO: fetch images
} else {
registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { isGranted ->
if (isGranted.all { it.value }) {
// TODO: fetch images
} else {
// TODO("replace permission denied process")
Toast.makeText(this, "permission error", Toast.LENGTH_SHORT).show()
finish()
}
}.launch(requiredPermissions)
}
}
private fun hasAllPermissions() = requiredPermissions.all {
ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED
}
}
onRequestPermissionsResult
가 deprecated 되었기에registerForActivityResult
로 처리
참조: 스택오버플로우
이제 처리할 이미지를 래핑할 모델을 생성한다
data class GalleryModel(
val uri: Uri,
val name: String,
val fullName: String,
val mimeType: String,
val addedDate: Long,
val folder: String,
val size: Long,
val width: Int,
val height: Int,
){
fun toGallery() = Gallery(
uri,
name,
fullName,
mimeType,
Date(addedDate),
folder,
size,
width,
height,
)
}
data class Gallery(
val uri: Uri,
val name: String,
val fullName: String,
val mimeType: String,
val addedDate: Date,
val folder: String,
val size: Long,
val width: Int,
val height: Int,
)
이미지 데이터에서 뽑을 건 모델에 정의된 대로 아래와 같다
uri
이미지 Uri,
name
이름,
fullName
확장자를 포함한 이름,
mimeType
Mime 형식의 type,
addedDate
저장된 날짜,
folder
저장된 폴더 명,
size
파일의 크기,
width
사진의 너비,
height
사진의 높이
PagingSource를 구현하기 이전에 데이터를 페이징 해서 가져오는 구문
import android.provider.MediaStore.Images.Media
class GalleryDataSourceImpl @Inject constructor(
@ApplicationContext context: Context
) : GalleryDataSource {
private val contentResolver = context.contentResolver
override fun fetchGalleryImages(limit: Int, offset: Int): List<GalleryModel> {
val contentUri = Media.EXTERNAL_CONTENT_URI
val projection = arrayOf(
Media._ID,
Media.TITLE,
Media.DISPLAY_NAME,
Media.MIME_TYPE,
Media.DATE_TAKEN,
Media.BUCKET_DISPLAY_NAME,
Media.SIZE,
Media.WIDTH,
Media.HEIGHT,
)
val galleryImage = mutableListOf<GalleryModel>()
val selection =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) Media.SIZE + " > 0"
else null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
contentResolver.query(
contentUri,
projection,
Bundle().apply {
// Limit & Offset
putInt(ContentResolver.QUERY_ARG_LIMIT, limit)
putInt(ContentResolver.QUERY_ARG_OFFSET, offset)
// Sort function
putStringArray(
ContentResolver.QUERY_ARG_SORT_COLUMNS,
arrayOf(Media.DATE_TAKEN)
)
putInt(
ContentResolver.QUERY_ARG_SORT_DIRECTION,
ContentResolver.QUERY_SORT_DIRECTION_DESCENDING
)
// Selection
putString(ContentResolver.QUERY_ARG_SQL_SELECTION, selection)
}, null
)
} else {
val sortOrder =
"${Media.DATE_TAKEN} DESC LIMIT $limit OFFSET $offset"
contentResolver.query(
contentUri,
projection,
selection,
null,
sortOrder
)
}?.use { cursor ->
while (cursor.moveToNext()) {
galleryImage.add(
GalleryModel(
uri = Uri.withAppendedPath(
contentUri,
cursor.getLong(cursor.getColumnIndexOrThrow(Media._ID)).toString()
),
name = cursor.getString(cursor.getColumnIndexOrThrow(Media.TITLE)),
fullName = cursor.getString(cursor.getColumnIndexOrThrow(Media.DISPLAY_NAME)),
mimeType = cursor.getString(cursor.getColumnIndexOrThrow(Media.MIME_TYPE)),
addedDate = cursor.getLong(cursor.getColumnIndexOrThrow(Media.DATE_TAKEN)),
folder = cursor.getString(cursor.getColumnIndexOrThrow(Media.BUCKET_DISPLAY_NAME)),
size = cursor.getLong(cursor.getColumnIndexOrThrow(Media.SIZE)),
width = cursor.getInt(cursor.getColumnIndexOrThrow(Media.WIDTH)),
height = cursor.getInt(cursor.getColumnIndexOrThrow(Media.HEIGHT)),
)
)
}
}![](https://velog.velcdn.com/images/dlwpdlf147/post/a0f7cad7-50f7-45a4-b4e0-de7482626044/image.mp4)
return galleryImage
}
}
기기 내의 데이터는 contentResolver로 접근을 해야한다
쿼리 형식은 아래와 같고Curosr
를 반환한다.
하지만 안드로이드 11부터는 LIMIT에 관한 쿼리 내용이 바뀌었기에 분기를 두고 진행한다. 참조
이미지 컬럼에 관한 문서
이제 구현한 DataSource를 가지고 PagingSource를 만들어 본다.
class GalleryRepositoryImpl @Inject constructor(
private val galleryLocalDataSource: GalleryLocalDataSource,
): GalleryRepository {
override fun getGalleryImagePagingSource():PagingSource<Int, Gallery> {
return object: PagingSource<Int, Gallery>() {
override fun getRefreshKey(state: PagingState<Int, Gallery>): Int? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Gallery> {
try {
val pageNumber = params.key ?: 0
val response = galleryLocalDataSource.fetchGalleryImages(params.loadSize, pageNumber*params.loadSize)
return LoadResult.Page(
data = response.map { it.toGallery() },
prevKey = if(pageNumber==0) null else pageNumber-1,
nextKey = if(response.isEmpty()) null else pageNumber+1
)
} catch (e: Exception) {
// TODO("error process")
throw e
}
}
}
}
}
PagingSource를 가지고 PagingAdapter 클래스를 구현한다.
recyclerView에 들어갈 item layout인 row_gallery_image.xml
파일을 생성했다고 가정한다
class GalleryRecyclerViewAdapter :
PagingDataAdapter<Gallery, GalleryRecyclerViewAdapter.ImageViewHolder>(diffCallback) {
companion object {
val diffCallback = object : DiffUtil.ItemCallback<Gallery>() {
override fun areItemsTheSame(oldItem: Gallery, newItem: Gallery) =
oldItem.uri == newItem.uri
override fun areContentsTheSame(oldItem: Gallery, newItem: Gallery) =
oldItem == newItem
}
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): ImageViewHolder = ImageViewHolder(
RowGalleryImageBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
val item = getItem(position)
item?.let {
holder.bind(it)
}
}
inner class ImageViewHolder(
private val binding: RowGalleryImageBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: Gallery) {
binding.run {
Glide.with(binding.root.context)
.load(item.uri)
.into(binding.rowGalleryImageView)
}
}
}
}
Pager를 보관할 ViewModel
@HiltViewModel
class GalleryViewModel @Inject constructor(
private val galleryImageFetch: GalleryRepository
): BaseViewModel() {
val galleryPager = Pager(
config = PagingConfig(pageSize = 50)
) {
galleryImageFetch.getGalleryImagePagingSource()
}.flow.cachedIn(viewModelScope)
}
이를 보여줄 Activity
// Activity
private fun setRecyclerView(){
galleryRecyclerViewAdapter = GalleryRecyclerViewAdapter()
bind.galleryRv.adapter = galleryRecyclerViewAdapter
lifecycleScope.launch {
viewModel.galleryPager.collectLatest { pagingData ->
galleryRecyclerViewAdapter.submitData(pagingData)
}
}
}
GalleryLocalDataSource, GalleryRepository 가 어떻게 짜여있는지 알수 있을까요 아니면 전체코드를 확인할수있는 git 등을 공유받을수 있을까요? GalleryDataSourceImpl는 어디서 사용하고있는지 써둔게 달라서 보면서 이해가 안되네용