feat(gallery): 로딩 다이얼로그 표시 및 이미지 캐싱 적용

Fragment에서 isLoading에 따라 Loading Dialog를 표시/해제.
Glide에 디스크 캐싱 적용으로 스크롤 성능 개선.
This commit is contained in:
2025-08-22 22:12:36 +09:00
parent e3ed816fb3
commit 9164942395
3 changed files with 73 additions and 39 deletions

View File

@@ -6,6 +6,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import kr.co.vividnext.sodalive.databinding.ItemCharacterGalleryBinding import kr.co.vividnext.sodalive.databinding.ItemCharacterGalleryBinding
class CharacterGalleryAdapter( class CharacterGalleryAdapter(
@@ -19,6 +20,7 @@ class CharacterGalleryAdapter(
fun bind(item: CharacterImageListItemResponse) { fun bind(item: CharacterImageListItemResponse) {
Glide.with(binding.ivImage) Glide.with(binding.ivImage)
.load(item.imageUrl) .load(item.imageUrl)
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
.into(binding.ivImage) .into(binding.ivImage)
if (item.isOwned) { if (item.isOwned) {

View File

@@ -10,6 +10,7 @@ import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.base.BaseFragment import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.chat.character.detail.CharacterDetailActivity.Companion.EXTRA_CHARACTER_ID import kr.co.vividnext.sodalive.chat.character.detail.CharacterDetailActivity.Companion.EXTRA_CHARACTER_ID
import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.FragmentCharacterGalleryBinding import kr.co.vividnext.sodalive.databinding.FragmentCharacterGalleryBinding
import kr.co.vividnext.sodalive.extensions.dpToPx import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
@@ -21,7 +22,9 @@ class CharacterGalleryFragment : BaseFragment<FragmentCharacterGalleryBinding>(
FragmentCharacterGalleryBinding::inflate FragmentCharacterGalleryBinding::inflate
) { ) {
private val viewModel: CharacterGalleryViewModel by viewModel() private val viewModel: CharacterGalleryViewModel by viewModel()
private lateinit var adapter: CharacterGalleryAdapter private lateinit var adapter: CharacterGalleryAdapter
private lateinit var loadingDialog: LoadingDialog
private val characterId: Long by lazy { private val characterId: Long by lazy {
arguments?.getLong("arg_character_id") arguments?.getLong("arg_character_id")
@@ -30,8 +33,11 @@ class CharacterGalleryFragment : BaseFragment<FragmentCharacterGalleryBinding>(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
setupRecyclerView() setupRecyclerView()
observeState() observeState()
viewModel.loadInitial(characterId) viewModel.loadInitial(characterId)
} }
@@ -70,6 +76,13 @@ class CharacterGalleryFragment : BaseFragment<FragmentCharacterGalleryBinding>(
View.GONE View.GONE
} }
// 로딩 다이얼로그 표시/해제
if (state.isLoading) {
loadingDialog.show(screenWidth)
} else {
hideLoadingDialog()
}
if (state.items.isNotEmpty() && !state.isLoading) { if (state.items.isNotEmpty() && !state.isLoading) {
binding.rvGallery.visibility = View.VISIBLE binding.rvGallery.visibility = View.VISIBLE
binding.clRatio.visibility = View.VISIBLE binding.clRatio.visibility = View.VISIBLE
@@ -117,4 +130,13 @@ class CharacterGalleryFragment : BaseFragment<FragmentCharacterGalleryBinding>(
viewModel.purchaseImage(item.id, position) viewModel.purchaseImage(item.id, position)
}.show() }.show()
} }
private fun hideLoadingDialog() {
loadingDialog.dismiss()
}
override fun onDestroyView() {
hideLoadingDialog()
super.onDestroyView()
}
} }

View File

@@ -39,7 +39,6 @@ class CharacterGalleryViewModel(
isLastPage = false isLastPage = false
isRequesting = false isRequesting = false
accumulatedItems.clear() accumulatedItems.clear()
_uiState.value = UiState(isLoading = true)
request(page = currentPage) request(page = currentPage)
} }
@@ -51,6 +50,7 @@ class CharacterGalleryViewModel(
private fun request(page: Int) { private fun request(page: Int) {
val token = "Bearer ${SharedPreferenceManager.token}" val token = "Bearer ${SharedPreferenceManager.token}"
isRequesting = true isRequesting = true
_uiState.value = _uiState.value?.copy(isLoading = isRequesting || isPurchasing)
compositeDisposable.add( compositeDisposable.add(
repository.getCharacterImageList( repository.getCharacterImageList(
token = token, token = token,
@@ -63,6 +63,9 @@ class CharacterGalleryViewModel(
.subscribe({ response -> .subscribe({ response ->
val success = response.success val success = response.success
val data = response.data val data = response.data
isRequesting = false
if (success && data != null) { if (success && data != null) {
// 누적 처리 // 누적 처리
val newItems = data.items val newItems = data.items
@@ -88,18 +91,17 @@ class CharacterGalleryViewModel(
) )
} else { } else {
_uiState.value = _uiState.value?.copy( _uiState.value = _uiState.value?.copy(
isLoading = false, isLoading = isRequesting || isPurchasing,
error = response.message ?: "갤러리 정보를 불러오지 못했습니다." error = response.message ?: "갤러리 정보를 불러오지 못했습니다."
) )
} }
isRequesting = false
}, { throwable -> }, { throwable ->
isRequesting = false
Logger.e(throwable, throwable.message ?: "") Logger.e(throwable, throwable.message ?: "")
_uiState.value = _uiState.value?.copy( _uiState.value = _uiState.value?.copy(
isLoading = false, isLoading = isRequesting || isPurchasing,
error = "네트워크 오류가 발생했습니다. 잠시 후 다시 시도해 주세요." error = "네트워크 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."
) )
isRequesting = false
}) })
) )
} }
@@ -112,13 +114,18 @@ class CharacterGalleryViewModel(
val token = "Bearer ${SharedPreferenceManager.token}" val token = "Bearer ${SharedPreferenceManager.token}"
isPurchasing = true isPurchasing = true
_uiState.value = _uiState.value?.copy(
isLoading = isRequesting || isPurchasing,
)
compositeDisposable.add( compositeDisposable.add(
repository.purchaseCharacterImage(token = token, imageId = imageId) repository.purchaseCharacterImage(token = token, imageId = imageId)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe({ response -> .subscribe(
{ response ->
val success = response.success val success = response.success
val data = response.data val data = response.data
isPurchasing = false
if (success && data != null) { if (success && data != null) {
// 응답 imageUrl로 교체, 소유 상태 true로 변경 // 응답 imageUrl로 교체, 소유 상태 true로 변경
val updated = target.copy( val updated = target.copy(
@@ -141,6 +148,7 @@ class CharacterGalleryViewModel(
ownedCount = ownedAfter, ownedCount = ownedAfter,
ratio = ratio, ratio = ratio,
items = accumulatedItems.toList(), items = accumulatedItems.toList(),
isLoading = isRequesting || isPurchasing,
error = null error = null
) )
} else { } else {
@@ -148,14 +156,16 @@ class CharacterGalleryViewModel(
error = response.message ?: "구매에 실패했습니다." error = response.message ?: "구매에 실패했습니다."
) )
} }
},
{ throwable ->
isPurchasing = false isPurchasing = false
}, { throwable ->
Logger.e(throwable, throwable.message ?: "") Logger.e(throwable, throwable.message ?: "")
_uiState.value = _uiState.value?.copy( _uiState.value = _uiState.value?.copy(
isLoading = isRequesting || isPurchasing,
error = "네트워크 오류가 발생했습니다. 잠시 후 다시 시도해 주세요." error = "네트워크 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."
) )
isPurchasing = false }
}) )
) )
} }
} }