일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- 플레이스토어
- 코딩테스트
- 커스텀뷰
- DiffUtil
- 뷰
- MVVM
- NavHost
- sharedFlow
- Flow
- Authentication
- Build variants
- UiState
- 리사이클러뷰
- NavController
- ListAdapter
- Compose
- coroutine
- 코틀린
- 안드로이드
- XML
- 알고리즘
- 클린아키텍처
- Jetpack
- 파이어베이스
- Kotlin
- 로그인
- 회원가입
- cleanarchitecture
- Android
- 컴포즈
- Today
- Total
Grusie 안드로이드 개발 기술 블로그
[Android] 이미지 저장 및 삭제 로직 공유 본문
플로우
이미지 리스트를 포함하여 서버통신을 하는 과정이 필요했으며, 중간에 임시저장을 할 수 있도록 구현 했어야 했다.
추 후 임시저장 리스트를 통해 불러 올 수 있어야 했기에 이미지 저장은 필수였다.
처음 생각은 origin 즉, 기본 이미지를 갤러리에서 가져오니, 그 uri를 그대로 저장하고, crop을 진행한 것들만 저장하면 되겠다고 생각하였다.
하지만 고민을 해보다가, 만약 원본 이미지를 갤러리에서 삭제하면 어떻게 되는거지? 라는 물음에 도달해서, originImage도 저장하고, cropImage도 저장하게 되었다.
저장은 앱의 내부 저장소를 이용할 것인데, 이미지가 계속 늘어나지 않도록, 화면을 종료하면 임시저장하지 않은 상태일 경우, 삭제하도록 구현하였다.
하지만, 만약 임시저장이 되지 않은 상태로, 앱을 강제 종료하면 메모리를 잡아먹는 이미지들이 계속 늘어날텐데?
이런 의문점이 계속 생겨서 구현하게 되었던 내용을 기록해보려고 한다.
해결해야 할 문제
1. 갤러리에서 가져온 원본 이미지 uri를 저장하는 것은 원본파일 삭제의 위험이 있을 것이다.
2. 원본 이미지와, 크롭한 이미지만 저장해야 하는데, 수정을 여러번 하면 할 때마다 생성되기에, 마지막에 저장하기로 한 이미지만 남기고 삭제 해주어야 한다.
3. 임시저장 기능을 사용하지 않았음에도, 이미지는 내부저장소에 저장이 될 것이기에, 임시저장을 하지 않고, 화면을 나가면 삭제 해주어야한다.
4. 앱을 강제 종료하면, 그 플로우를 타지 않아, 쓰레기값이 나올 것이고, 그것들도 삭제해주어야 한다.
갤러리 이미지 Uri => Bitmap
private fun uriToBitmap(uri: Uri): Bitmap? {
return try {
val parcelFileDescriptor = context.contentResolver.openFileDescriptor(uri, "r")
parcelFileDescriptor?.use {
val fileDescriptor = it.fileDescriptor
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options)
val orientation = getOrientation(uri)
BitmapFactory.decodeFileDescriptor(fileDescriptor).let { bitmap ->
rotateBitmap(bitmap, orientation)
}
}
} catch (e: Exception) {
setUiState(ImageCropUiState.Error("이미지 처리 중 에러가 발생했습니다. 다시 시도해주세요."))
Log.e("confirm uriToBimapError", "${e.message}")
null
}
}
갤러리 이미지Uri로 Bitmap을 만드는 과정이다. 중간에 회전이 되길래, getOrientation() 함수와, rotateBitmap()함수를 만들어서, 정상적으로 회전시켜 저장했다.
이 과정은 갤러리에서 이미지를 선택해서, 이미지 크롭 화면으로 넘어갈 때 작동한다.(이미지 크롭 화면에서 뒤로가기를 누르면 다시 삭제된다.)
비트맵 저장 후, URI리스트 관리
private suspend fun setBitmapImageList(originBitmapList: List<Bitmap>) {
setUiState(ImageCropUiState.Loading)
originBitmapList.let {
saveBitmapList(originBitmapList).onSuccess {
setOriginUriList(it)
croppedImageUriList = it.toMutableList()
setUiState(ImageCropUiState.Success(completeFlag = false))
}.onFailure {
Log.e(this::class.simpleName, it.message ?: "")
setUiState(ImageCropUiState.Error("이미치 처리 중 문제가 발생했습니다. 다시 시도해주세요."))
}
_croppedImageList.emit(originBitmapList)
if (_croppedImageList.value.isNotEmpty()) {
_currentImgPosition.emit(0)
}
}
}
저렇게 받아온 비트맵 이미지 리스트들로, 이전에서 받아온 저장될 디렉토리 위치에 저장하고, 저장된 uri를 원본이미지 uri리스트와, Crop이미지 Uri 리스트에 각각 넣어준다.
비트맵 저장 로직
private suspend fun saveBitmapList(bitmapList: List<Bitmap?>): Result<List<String>> {
setUiState(ImageCropUiState.Loading)
return withContext(Dispatchers.IO) {
imageDir?.let { imageDir ->
val directory = File(context.filesDir, imageDir)
if (!directory.exists()) {
directory.mkdirs()
}
val tempUriList = mutableListOf<String>()
bitmapList.forEach {
it?.let { bitmap ->
val file = File(directory, "temp_cropped_image_${UUID.randomUUID()}.jpg")
var outputStream: FileOutputStream? = null
try {
outputStream = FileOutputStream(file)
bitmap.compress(
Bitmap.CompressFormat.JPEG,
100,
outputStream
)
tempUriList.add(Uri.fromFile(file).toString())
} catch (e: IOException) {
e.printStackTrace()
Result.failure<IOException>(e)
} finally {
outputStream?.close()
}
}
}
Result.success(tempUriList)
} ?: Result.failure(Exception())
}
}
이미지 저장 로직은 이렇게이다. 정해진 이미지 저장 디렉토리 (필자는 files 하위에 crop_images디렉토리를 만들어, 그 하위에 각 해당하는 폴더별로 이미지들을 저장하도록 하였다.)
이미지 저장 경로
저장 이미지는, 이미지 원본 파일과 크롭 된 이미지 이렇게 저장을 하였다.
그렇게 저장한 이미지들의 uri를 가지고, 다음 화면에서 이미지 리스트를 보여주었으며,
크롭 이미지의 수정/삭제 시에도 삭제되도록 구현해도 되지만, 임시저장/서버전송에서 필요한 이미지를 제외하고는 삭제하도록 구현 되어 있다.
사용하지 않는 파일들을 삭제하는 로직
private fun getAttachs(): List<File>? {
return subPath?.let { imageDir ->
val result = mutableListOf<File>()
val leafFileDir = File(context.filesDir, ImageCropView.CROP_IMAGES_PATH + imageDir)
if (leafFileDir.exists() && leafFileDir.isDirectory) {
leafFileDir.listFiles()?.forEach { file ->
val fileUri = Uri.fromFile(file)
val isFileUriInList = _photoList.value.any { photoSimpleItem ->
photoSimpleItem.uri == fileUri.toString()
} || _photoList.value.any { photoSimpleItem ->
photoSimpleItem.originUri == fileUri.toString()
}
if (isFileUriInList) {
result.add(file)
} else {
file.delete()
}
}
}
result
}
}
파일 디렉토리에서, 필요한 이미지를 제외하고는 제거하는 로직
화면이 종료될 때 이미지 삭제 처리
override fun onCleared() {
super.onCleared()
if (_deleteFlag) subPath?.let { clearCropDir(it) }
}
private fun clearCropDir(deleteDir: String) {
val leafFileDir = File(context.filesDir, ImageCropView.CROP_IMAGES_PATH + deleteDir)
leafFileDir.deleteRecursively()
}
화면을 종료할 시(뷰모델이 종료될 시) 임시저장을 하지 않은 상태(_deleteFlag로 구분)라면 디렉토리에 있는 이미지를 전부 날리도록 구현 되어 있다.
강제종료 시 남은 이미지들을 처리하기 위한 로직
private fun deleteUnUsedDir() {
viewModelScope.launch {
getTempSavedImageDirUseCase(_spaceId).onSuccess { usedPathList ->
val rootDir = File(context.filesDir, ImageCropView.CROP_IMAGES_PATH)
rootDir.listFiles()?.forEach { dir ->
val isUsedFlag = usedPathList.any { it == dir.name }
if (!isUsedFlag) {
dir.deleteRecursively()
}
}
}
initFlag = false
}
}
만약 앱을 강제종료해서, 이미지 디렉토리가 남아있을 경우를 대비하여, 화면에 처음 들어올 시, 임시저장 리스트의 이미지 저장 디렉토리들을 불러와서, 해당하지 않는 디렉토리들은 날리도록 구현하였다.
트래블맵 임시저장을 위한 DB
임시저장 DB에서 imageDir를 통해 사용하는 이미지폴더들을 관리한다.
후기
파일을 건드리는 것은, 로직도 복잡하고 생각해야 할 점들이 너무 많다.
쓸데 없는 데이터가 남아있는 것을 싫어하는 성격 탓에 용량이 늘어나는 것을 방지하고자 고민했던 것을 기록하였으며,
이렇게 하고 나니 뿌듯하다는 생각을 하였다. 특히 ViewModel에서 관리를 하기에, 화면회전/다크모드 진입 시 오류가 발생하지 않도록 하였으며, 이미지의 수정/삭제 기능까지 생각했어야 해서 머리가 아팠던 것 같다.
화면에 진입할 때, 이미지를 날리는 것과, 화면에서 나갈 때 날리는 것 중 고민을 하였으나...
임시저장을 위해서는 각각을 디렉토리로 관리를 했어야했고, 그렇기에 처음 들어올 때는 강제종료 등으로 삭제되지 않아, 사용하지 않는 디렉토리들을 날리고 화면을 나갈 때에는 현재 화면에 해당하는 이미지들만 날리면 되어서 구현하기 수월했다.
'안드로이드 개발' 카테고리의 다른 글
[Android] Build Variants로 개발단계에서 release 버전 테스트 하기 (0) | 2024.07.04 |
---|---|
[Android] 뷰모델에 런타임 오브젝트 의존성 주입하기(@AssistedInject) (1) | 2024.06.26 |
[Android] 커스텀 갤러리 만들기(이미지 불러오기, 다중 이미지 선택) (0) | 2024.05.27 |
[Android] release 버전에서 Gson 파싱이 안 되던 오류(코드 난독화 문제) (0) | 2024.05.07 |
[Android] 플레이 스토어 버전 업데이트 관리하기 (1) | 2024.05.07 |