feat(gallery): 로딩 다이얼로그 표시 및 이미지 캐싱 적용
Fragment에서 isLoading에 따라 Loading Dialog를 표시/해제. Glide에 디스크 캐싱 적용으로 스크롤 성능 개선.
This commit is contained in:
@@ -6,6 +6,7 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import kr.co.vividnext.sodalive.databinding.ItemCharacterGalleryBinding
|
||||
|
||||
class CharacterGalleryAdapter(
|
||||
@@ -19,6 +20,7 @@ class CharacterGalleryAdapter(
|
||||
fun bind(item: CharacterImageListItemResponse) {
|
||||
Glide.with(binding.ivImage)
|
||||
.load(item.imageUrl)
|
||||
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
|
||||
.into(binding.ivImage)
|
||||
|
||||
if (item.isOwned) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
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.common.GridSpacingItemDecoration
|
||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.databinding.FragmentCharacterGalleryBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
@@ -21,7 +22,9 @@ class CharacterGalleryFragment : BaseFragment<FragmentCharacterGalleryBinding>(
|
||||
FragmentCharacterGalleryBinding::inflate
|
||||
) {
|
||||
private val viewModel: CharacterGalleryViewModel by viewModel()
|
||||
|
||||
private lateinit var adapter: CharacterGalleryAdapter
|
||||
private lateinit var loadingDialog: LoadingDialog
|
||||
|
||||
private val characterId: Long by lazy {
|
||||
arguments?.getLong("arg_character_id")
|
||||
@@ -30,8 +33,11 @@ class CharacterGalleryFragment : BaseFragment<FragmentCharacterGalleryBinding>(
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
|
||||
setupRecyclerView()
|
||||
observeState()
|
||||
|
||||
viewModel.loadInitial(characterId)
|
||||
}
|
||||
|
||||
@@ -70,6 +76,13 @@ class CharacterGalleryFragment : BaseFragment<FragmentCharacterGalleryBinding>(
|
||||
View.GONE
|
||||
}
|
||||
|
||||
// 로딩 다이얼로그 표시/해제
|
||||
if (state.isLoading) {
|
||||
loadingDialog.show(screenWidth)
|
||||
} else {
|
||||
hideLoadingDialog()
|
||||
}
|
||||
|
||||
if (state.items.isNotEmpty() && !state.isLoading) {
|
||||
binding.rvGallery.visibility = View.VISIBLE
|
||||
binding.clRatio.visibility = View.VISIBLE
|
||||
@@ -117,4 +130,13 @@ class CharacterGalleryFragment : BaseFragment<FragmentCharacterGalleryBinding>(
|
||||
viewModel.purchaseImage(item.id, position)
|
||||
}.show()
|
||||
}
|
||||
|
||||
private fun hideLoadingDialog() {
|
||||
loadingDialog.dismiss()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
hideLoadingDialog()
|
||||
super.onDestroyView()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ class CharacterGalleryViewModel(
|
||||
isLastPage = false
|
||||
isRequesting = false
|
||||
accumulatedItems.clear()
|
||||
_uiState.value = UiState(isLoading = true)
|
||||
request(page = currentPage)
|
||||
}
|
||||
|
||||
@@ -51,6 +50,7 @@ class CharacterGalleryViewModel(
|
||||
private fun request(page: Int) {
|
||||
val token = "Bearer ${SharedPreferenceManager.token}"
|
||||
isRequesting = true
|
||||
_uiState.value = _uiState.value?.copy(isLoading = isRequesting || isPurchasing)
|
||||
compositeDisposable.add(
|
||||
repository.getCharacterImageList(
|
||||
token = token,
|
||||
@@ -63,6 +63,9 @@ class CharacterGalleryViewModel(
|
||||
.subscribe({ response ->
|
||||
val success = response.success
|
||||
val data = response.data
|
||||
|
||||
isRequesting = false
|
||||
|
||||
if (success && data != null) {
|
||||
// 누적 처리
|
||||
val newItems = data.items
|
||||
@@ -88,18 +91,17 @@ class CharacterGalleryViewModel(
|
||||
)
|
||||
} else {
|
||||
_uiState.value = _uiState.value?.copy(
|
||||
isLoading = false,
|
||||
isLoading = isRequesting || isPurchasing,
|
||||
error = response.message ?: "갤러리 정보를 불러오지 못했습니다."
|
||||
)
|
||||
}
|
||||
isRequesting = false
|
||||
}, { throwable ->
|
||||
isRequesting = false
|
||||
Logger.e(throwable, throwable.message ?: "")
|
||||
_uiState.value = _uiState.value?.copy(
|
||||
isLoading = false,
|
||||
isLoading = isRequesting || isPurchasing,
|
||||
error = "네트워크 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."
|
||||
)
|
||||
isRequesting = false
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -112,50 +114,58 @@ class CharacterGalleryViewModel(
|
||||
|
||||
val token = "Bearer ${SharedPreferenceManager.token}"
|
||||
isPurchasing = true
|
||||
_uiState.value = _uiState.value?.copy(
|
||||
isLoading = isRequesting || isPurchasing,
|
||||
)
|
||||
compositeDisposable.add(
|
||||
repository.purchaseCharacterImage(token = token, imageId = imageId)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ response ->
|
||||
val success = response.success
|
||||
val data = response.data
|
||||
if (success && data != null) {
|
||||
// 응답 imageUrl로 교체, 소유 상태 true로 변경
|
||||
val updated = target.copy(
|
||||
imageUrl = data.imageUrl,
|
||||
isOwned = true
|
||||
)
|
||||
accumulatedItems[position] = updated
|
||||
.subscribe(
|
||||
{ response ->
|
||||
val success = response.success
|
||||
val data = response.data
|
||||
isPurchasing = false
|
||||
if (success && data != null) {
|
||||
// 응답 imageUrl로 교체, 소유 상태 true로 변경
|
||||
val updated = target.copy(
|
||||
imageUrl = data.imageUrl,
|
||||
isOwned = true
|
||||
)
|
||||
accumulatedItems[position] = updated
|
||||
|
||||
val total = _uiState.value?.totalCount ?: accumulatedItems.size.toLong()
|
||||
val ownedBefore = _uiState.value?.ownedCount
|
||||
?: accumulatedItems.count { it.isOwned }.toLong()
|
||||
val ownedAfter = ownedBefore + 1
|
||||
val ratio = if (total > 0) {
|
||||
ownedAfter.toFloat() / total.toFloat()
|
||||
val total = _uiState.value?.totalCount ?: accumulatedItems.size.toLong()
|
||||
val ownedBefore = _uiState.value?.ownedCount
|
||||
?: accumulatedItems.count { it.isOwned }.toLong()
|
||||
val ownedAfter = ownedBefore + 1
|
||||
val ratio = if (total > 0) {
|
||||
ownedAfter.toFloat() / total.toFloat()
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
|
||||
_uiState.value = _uiState.value?.copy(
|
||||
ownedCount = ownedAfter,
|
||||
ratio = ratio,
|
||||
items = accumulatedItems.toList(),
|
||||
isLoading = isRequesting || isPurchasing,
|
||||
error = null
|
||||
)
|
||||
} else {
|
||||
0f
|
||||
_uiState.value = _uiState.value?.copy(
|
||||
error = response.message ?: "구매에 실패했습니다."
|
||||
)
|
||||
}
|
||||
|
||||
},
|
||||
{ throwable ->
|
||||
isPurchasing = false
|
||||
Logger.e(throwable, throwable.message ?: "")
|
||||
_uiState.value = _uiState.value?.copy(
|
||||
ownedCount = ownedAfter,
|
||||
ratio = ratio,
|
||||
items = accumulatedItems.toList(),
|
||||
error = null
|
||||
)
|
||||
} else {
|
||||
_uiState.value = _uiState.value?.copy(
|
||||
error = response.message ?: "구매에 실패했습니다."
|
||||
isLoading = isRequesting || isPurchasing,
|
||||
error = "네트워크 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."
|
||||
)
|
||||
}
|
||||
isPurchasing = false
|
||||
}, { throwable ->
|
||||
Logger.e(throwable, throwable.message ?: "")
|
||||
_uiState.value = _uiState.value?.copy(
|
||||
error = "네트워크 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."
|
||||
)
|
||||
isPurchasing = false
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user